| /* |
| * 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 static java.util.Objects.requireNonNull; |
| |
| import android.annotation.NonNull; |
| import android.annotation.RequiresPermission; |
| import android.annotation.SystemApi; |
| 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 android.util.Pair; |
| import android.util.Slog; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.util.Preconditions; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.concurrent.Executor; |
| |
| /** |
| * 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(); |
| |
| /** |
| * @hide |
| */ |
| @VisibleForTesting |
| public Map<ConversationListener, Pair<Executor, IConversationListener>> |
| mConversationListeners = new HashMap<>(); |
| |
| @NonNull |
| private Context mContext; |
| |
| @NonNull |
| private IPeopleManager mService; |
| |
| /** |
| * @hide |
| */ |
| public PeopleManager(@NonNull Context context) throws ServiceManager.ServiceNotFoundException { |
| mContext = context; |
| mService = IPeopleManager.Stub.asInterface(ServiceManager.getServiceOrThrow( |
| Context.PEOPLE_SERVICE)); |
| } |
| |
| /** |
| * @hide |
| */ |
| @VisibleForTesting |
| public PeopleManager(@NonNull Context context, IPeopleManager service) { |
| mContext = context; |
| mService = service; |
| } |
| |
| /** |
| * Returns whether a shortcut has a conversation associated. |
| * |
| * <p>Requires android.permission.READ_PEOPLE_DATA permission. |
| * |
| * <p>This method may return different results for the same shortcut over time, as an app adopts |
| * conversation features or if a user hasn't communicated with the conversation associated to |
| * the shortcut in a while, so the result should not be stored and relied on indefinitely by |
| * clients. |
| * |
| * @param packageName name of the package the conversation is part of |
| * @param shortcutId the shortcut id backing the conversation |
| * @return whether the {@shortcutId} is backed by a Conversation. |
| * @hide |
| */ |
| @SystemApi |
| @RequiresPermission(android.Manifest.permission.READ_PEOPLE_DATA) |
| public boolean isConversation(@NonNull String packageName, @NonNull String shortcutId) { |
| Preconditions.checkStringNotEmpty(packageName); |
| Preconditions.checkStringNotEmpty(shortcutId); |
| try { |
| return mService.isConversation(packageName, mContext.getUserId(), shortcutId); |
| } catch (RemoteException e) { |
| throw e.rethrowFromSystemServer(); |
| } |
| } |
| |
| /** |
| * 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<>(); |
| } |
| |
| /** |
| * Listeners for conversation changes. |
| * |
| * @hide |
| */ |
| public interface ConversationListener { |
| /** |
| * Triggers when the conversation registered for a listener has been updated. |
| * |
| * @param conversation The conversation with modified data |
| * @see IPeopleManager#registerConversationListener(String, int, String, |
| * android.app.people.ConversationListener) |
| * |
| * <p>Only system root and SysUI have access to register the listener. |
| */ |
| default void onConversationUpdate(@NonNull ConversationChannel conversation) { |
| } |
| } |
| |
| /** |
| * Register a listener to watch for changes to the conversation identified by {@code |
| * packageName}, {@code userId}, and {@code shortcutId}. |
| * |
| * @param packageName The package name to match and filter the conversation to send updates for. |
| * @param userId The user ID to match and filter the conversation to send updates for. |
| * @param shortcutId The shortcut ID to match and filter the conversation to send updates for. |
| * @param listener The listener to register to receive conversation updates. |
| * @param executor {@link Executor} to handle the listeners. To dispatch listeners to the |
| * main thread of your application, you can use |
| * {@link android.content.Context#getMainExecutor()}. |
| * @hide |
| */ |
| public void registerConversationListener(String packageName, int userId, String shortcutId, |
| ConversationListener listener, Executor executor) { |
| requireNonNull(listener, "Listener cannot be null"); |
| requireNonNull(packageName, "Package name cannot be null"); |
| requireNonNull(shortcutId, "Shortcut ID cannot be null"); |
| synchronized (mConversationListeners) { |
| IConversationListener proxy = (IConversationListener) new ConversationListenerProxy( |
| executor, listener); |
| try { |
| mService.registerConversationListener( |
| packageName, userId, shortcutId, proxy); |
| mConversationListeners.put(listener, |
| new Pair<>(executor, proxy)); |
| } catch (RemoteException e) { |
| throw e.rethrowFromSystemServer(); |
| } |
| } |
| } |
| |
| /** |
| * Unregisters the listener previously registered to watch conversation changes. |
| * |
| * @param listener The listener to register to receive conversation updates. |
| * @hide |
| */ |
| public void unregisterConversationListener( |
| ConversationListener listener) { |
| requireNonNull(listener, "Listener cannot be null"); |
| |
| synchronized (mConversationListeners) { |
| if (mConversationListeners.containsKey(listener)) { |
| IConversationListener proxy = mConversationListeners.remove(listener).second; |
| try { |
| mService.unregisterConversationListener(proxy); |
| } catch (RemoteException e) { |
| throw e.rethrowFromSystemServer(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Listener proxy class for {@link ConversationListener} |
| * |
| * @hide |
| */ |
| private static class ConversationListenerProxy extends |
| IConversationListener.Stub { |
| private final Executor mExecutor; |
| private final ConversationListener mListener; |
| |
| ConversationListenerProxy(Executor executor, ConversationListener listener) { |
| mExecutor = executor; |
| mListener = listener; |
| } |
| |
| @Override |
| public void onConversationUpdate(@NonNull ConversationChannel conversation) { |
| if (mListener == null || mExecutor == null) { |
| // Binder is dead. |
| Slog.e(LOG_TAG, "Binder is dead"); |
| return; |
| } |
| mExecutor.execute(() -> mListener.onConversationUpdate(conversation)); |
| } |
| } |
| } |