Loading core/java/android/app/people/IConversationListener.aidl 0 → 100644 +33 −0 Original line number Diff line number Diff line /** * 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.app.people.ConversationChannel; import android.content.pm.ParceledListSlice; import android.os.UserHandle; import java.util.List; /** * Interface for PeopleManager#ConversationListener. * * @hide */ oneway interface IConversationListener { void onConversationUpdate(in ConversationChannel conversation); } No newline at end of file core/java/android/app/people/IPeopleManager.aidl +3 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package android.app.people; import android.app.people.ConversationStatus; import android.app.people.ConversationChannel; import android.app.people.IConversationListener; import android.content.pm.ParceledListSlice; import android.net.Uri; import android.os.IBinder; Loading Loading @@ -62,4 +63,6 @@ interface IPeopleManager { 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); void registerConversationListener(in String packageName, int userId, in String shortcutId, in IConversationListener callback); void unregisterConversationListener(in IConversationListener callback); } core/java/android/app/people/PeopleManager.java +130 −9 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package android.app.people; import static java.util.Objects.requireNonNull; import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.annotation.SystemApi; Loading @@ -25,12 +27,18 @@ 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. Loading @@ -40,11 +48,18 @@ 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 final Context mContext; private Context mContext; @NonNull private final IPeopleManager mService; private IPeopleManager mService; /** * @hide Loading @@ -55,6 +70,15 @@ public final class PeopleManager { Context.PEOPLE_SERVICE)); } /** * @hide */ @VisibleForTesting public PeopleManager(@NonNull Context context, IPeopleManager service) { mContext = context; mService = service; } /** * Returns whether a shortcut has a conversation associated. * Loading @@ -68,7 +92,6 @@ public final class PeopleManager { * @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 Loading @@ -95,7 +118,6 @@ public final class PeopleManager { * @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, Loading @@ -115,8 +137,8 @@ public final class PeopleManager { * * @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 * @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); Loading Loading @@ -164,4 +186,103 @@ public final class PeopleManager { } 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)); } } } core/tests/coretests/src/android/app/people/PeopleManagerTest.java 0 → 100644 +120 −0 Original line number Diff line number Diff line /* * 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 org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; import android.content.pm.ShortcutInfo; import android.os.test.TestLooper; import android.util.Pair; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.Map; import java.util.concurrent.Executor; /** * Tests for {@link android.app.people.PeopleManager.ConversationListener} and relevant APIs. */ @RunWith(AndroidJUnit4.class) public class PeopleManagerTest { private static final String CONVERSATION_ID_1 = "12"; private static final String CONVERSATION_ID_2 = "123"; private Context mContext; private final TestLooper mTestLooper = new TestLooper(); @Mock private IPeopleManager mService; private PeopleManager mPeopleManager; @Before public void setUp() throws Exception { mContext = InstrumentationRegistry.getContext(); MockitoAnnotations.initMocks(this); mPeopleManager = new PeopleManager(mContext, mService); } @Test public void testCorrectlyMapsToProxyConversationListener() throws Exception { PeopleManager.ConversationListener listenerForConversation1 = mock( PeopleManager.ConversationListener.class); registerListener(CONVERSATION_ID_1, listenerForConversation1); PeopleManager.ConversationListener listenerForConversation2 = mock( PeopleManager.ConversationListener.class); registerListener(CONVERSATION_ID_2, listenerForConversation2); Map<PeopleManager.ConversationListener, Pair<Executor, IConversationListener>> listenersToProxy = mPeopleManager.mConversationListeners; Pair<Executor, IConversationListener> listener = listenersToProxy.get( listenerForConversation1); ConversationChannel conversation = getConversation(CONVERSATION_ID_1); listener.second.onConversationUpdate(getConversation(CONVERSATION_ID_1)); mTestLooper.dispatchAll(); // Only call the associated listener. verify(listenerForConversation2, never()).onConversationUpdate(any()); // Should update the listeners mapped to the proxy. ArgumentCaptor<ConversationChannel> capturedConversation = ArgumentCaptor.forClass( ConversationChannel.class); verify(listenerForConversation1, times(1)).onConversationUpdate( capturedConversation.capture()); ConversationChannel conversationChannel = capturedConversation.getValue(); assertEquals(conversationChannel.getShortcutInfo().getId(), CONVERSATION_ID_1); assertEquals(conversationChannel.getShortcutInfo().getLabel(), conversation.getShortcutInfo().getLabel()); } private ConversationChannel getConversation(String shortcutId) { ShortcutInfo shortcutInfo = new ShortcutInfo.Builder(mContext, shortcutId).setLongLabel( "name").build(); NotificationChannel notificationChannel = new NotificationChannel("123", "channel", NotificationManager.IMPORTANCE_DEFAULT); return new ConversationChannel(shortcutInfo, 0, notificationChannel, null, 123L, false); } private void registerListener(String conversationId, PeopleManager.ConversationListener listener) { mPeopleManager.registerConversationListener(mContext.getPackageName(), mContext.getUserId(), conversationId, listener, mTestLooper.getNewExecutor()); } } services/people/java/com/android/server/people/PeopleService.java +141 −2 Original line number Diff line number Diff line Loading @@ -23,6 +23,7 @@ import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.people.ConversationChannel; import android.app.people.ConversationStatus; import android.app.people.IConversationListener; import android.app.people.IPeopleManager; import android.app.prediction.AppPredictionContext; import android.app.prediction.AppPredictionSessionId; Loading @@ -33,10 +34,12 @@ import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ParceledListSlice; import android.content.pm.ShortcutInfo; import android.os.Binder; import android.os.CancellationSignal; import android.os.IBinder; import android.os.Process; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.UserHandle; import android.util.ArrayMap; Loading @@ -47,6 +50,7 @@ import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.people.data.DataManager; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; Loading @@ -58,7 +62,9 @@ public class PeopleService extends SystemService { private static final String TAG = "PeopleService"; private final DataManager mDataManager; private DataManager mDataManager; @VisibleForTesting ConversationListenerHelper mConversationListenerHelper; private PackageManagerInternal mPackageManagerInternal; Loading @@ -71,6 +77,8 @@ public class PeopleService extends SystemService { super(context); mDataManager = new DataManager(context); mConversationListenerHelper = new ConversationListenerHelper(); mDataManager.addConversationsListener(mConversationListenerHelper); } @Override Loading Loading @@ -148,12 +156,14 @@ public class PeopleService extends SystemService { * @param message used as message if SecurityException is thrown * @throws SecurityException if the caller is not system or root */ private static void enforceSystemRootOrSystemUI(Context context, String message) { @VisibleForTesting protected void enforceSystemRootOrSystemUI(Context context, String message) { if (isSystemOrRoot()) return; context.enforceCallingPermission(android.Manifest.permission.STATUS_BAR_SERVICE, message); } @VisibleForTesting final IBinder mService = new IPeopleManager.Stub() { @Override Loading Loading @@ -241,8 +251,137 @@ public class PeopleService extends SystemService { return new ParceledListSlice<>( mDataManager.getStatuses(packageName, userId, conversationId)); } @Override public void registerConversationListener( String packageName, int userId, String shortcutId, IConversationListener listener) { enforceSystemRootOrSystemUI(getContext(), "register conversation listener"); mConversationListenerHelper.addConversationListener( new ListenerKey(packageName, userId, shortcutId), listener); } @Override public void unregisterConversationListener(IConversationListener listener) { enforceSystemRootOrSystemUI(getContext(), "unregister conversation listener"); mConversationListenerHelper.removeConversationListener(listener); } }; /** * Listeners for conversation changes. * * @hide */ public interface ConversationsListener { /** * Triggers with the list of modified conversations from {@link DataManager} for dispatching * relevant updates to clients. * * @param conversations The conversations with modified data * @see IPeopleManager#registerConversationListener(String, int, String, * android.app.people.ConversationListener) */ default void onConversationsUpdate(@NonNull List<ConversationChannel> conversations) { } } /** * Implements {@code ConversationListenerHelper} to dispatch conversation updates to registered * clients. */ public static class ConversationListenerHelper implements ConversationsListener { ConversationListenerHelper() { } @VisibleForTesting final RemoteCallbackList<IConversationListener> mListeners = new RemoteCallbackList<>(); /** Adds {@code listener} with {@code key} associated. */ public synchronized void addConversationListener(ListenerKey key, IConversationListener listener) { mListeners.unregister(listener); mListeners.register(listener, key); } /** Removes {@code listener}. */ public synchronized void removeConversationListener( IConversationListener listener) { mListeners.unregister(listener); } @Override /** Dispatches updates to {@code mListeners} with keys mapped to {@code conversations}. */ public void onConversationsUpdate(List<ConversationChannel> conversations) { int count = mListeners.beginBroadcast(); // Early opt-out if no listeners are registered. if (count == 0) { return; } Map<ListenerKey, ConversationChannel> keyedConversations = new HashMap<>(); for (ConversationChannel conversation : conversations) { keyedConversations.put(getListenerKey(conversation), conversation); } for (int i = 0; i < count; i++) { final ListenerKey listenerKey = (ListenerKey) mListeners.getBroadcastCookie(i); if (!keyedConversations.containsKey(listenerKey)) { continue; } final IConversationListener listener = mListeners.getBroadcastItem(i); try { ConversationChannel channel = keyedConversations.get(listenerKey); listener.onConversationUpdate(channel); } catch (RemoteException e) { // The RemoteCallbackList will take care of removing the dead object. } } mListeners.finishBroadcast(); } private ListenerKey getListenerKey(ConversationChannel conversation) { ShortcutInfo info = conversation.getShortcutInfo(); return new ListenerKey(info.getPackage(), info.getUserId(), info.getId()); } } private static class ListenerKey { private final String mPackageName; private final Integer mUserId; private final String mShortcutId; ListenerKey(String packageName, Integer userId, String shortcutId) { this.mPackageName = packageName; this.mUserId = userId; this.mShortcutId = shortcutId; } public String getPackageName() { return mPackageName; } public Integer getUserId() { return mUserId; } public String getShortcutId() { return mShortcutId; } @Override public boolean equals(Object o) { ListenerKey key = (ListenerKey) o; return key.getPackageName().equals(mPackageName) && key.getUserId() == mUserId && key.getShortcutId().equals(mShortcutId); } @Override public int hashCode() { return mPackageName.hashCode() + mUserId.hashCode() + mShortcutId.hashCode(); } } @VisibleForTesting final class LocalService extends PeopleServiceInternal { Loading Loading
core/java/android/app/people/IConversationListener.aidl 0 → 100644 +33 −0 Original line number Diff line number Diff line /** * 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.app.people.ConversationChannel; import android.content.pm.ParceledListSlice; import android.os.UserHandle; import java.util.List; /** * Interface for PeopleManager#ConversationListener. * * @hide */ oneway interface IConversationListener { void onConversationUpdate(in ConversationChannel conversation); } No newline at end of file
core/java/android/app/people/IPeopleManager.aidl +3 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package android.app.people; import android.app.people.ConversationStatus; import android.app.people.ConversationChannel; import android.app.people.IConversationListener; import android.content.pm.ParceledListSlice; import android.net.Uri; import android.os.IBinder; Loading Loading @@ -62,4 +63,6 @@ interface IPeopleManager { 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); void registerConversationListener(in String packageName, int userId, in String shortcutId, in IConversationListener callback); void unregisterConversationListener(in IConversationListener callback); }
core/java/android/app/people/PeopleManager.java +130 −9 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package android.app.people; import static java.util.Objects.requireNonNull; import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.annotation.SystemApi; Loading @@ -25,12 +27,18 @@ 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. Loading @@ -40,11 +48,18 @@ 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 final Context mContext; private Context mContext; @NonNull private final IPeopleManager mService; private IPeopleManager mService; /** * @hide Loading @@ -55,6 +70,15 @@ public final class PeopleManager { Context.PEOPLE_SERVICE)); } /** * @hide */ @VisibleForTesting public PeopleManager(@NonNull Context context, IPeopleManager service) { mContext = context; mService = service; } /** * Returns whether a shortcut has a conversation associated. * Loading @@ -68,7 +92,6 @@ public final class PeopleManager { * @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 Loading @@ -95,7 +118,6 @@ public final class PeopleManager { * @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, Loading @@ -115,8 +137,8 @@ public final class PeopleManager { * * @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 * @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); Loading Loading @@ -164,4 +186,103 @@ public final class PeopleManager { } 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)); } } }
core/tests/coretests/src/android/app/people/PeopleManagerTest.java 0 → 100644 +120 −0 Original line number Diff line number Diff line /* * 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 org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; import android.content.pm.ShortcutInfo; import android.os.test.TestLooper; import android.util.Pair; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.Map; import java.util.concurrent.Executor; /** * Tests for {@link android.app.people.PeopleManager.ConversationListener} and relevant APIs. */ @RunWith(AndroidJUnit4.class) public class PeopleManagerTest { private static final String CONVERSATION_ID_1 = "12"; private static final String CONVERSATION_ID_2 = "123"; private Context mContext; private final TestLooper mTestLooper = new TestLooper(); @Mock private IPeopleManager mService; private PeopleManager mPeopleManager; @Before public void setUp() throws Exception { mContext = InstrumentationRegistry.getContext(); MockitoAnnotations.initMocks(this); mPeopleManager = new PeopleManager(mContext, mService); } @Test public void testCorrectlyMapsToProxyConversationListener() throws Exception { PeopleManager.ConversationListener listenerForConversation1 = mock( PeopleManager.ConversationListener.class); registerListener(CONVERSATION_ID_1, listenerForConversation1); PeopleManager.ConversationListener listenerForConversation2 = mock( PeopleManager.ConversationListener.class); registerListener(CONVERSATION_ID_2, listenerForConversation2); Map<PeopleManager.ConversationListener, Pair<Executor, IConversationListener>> listenersToProxy = mPeopleManager.mConversationListeners; Pair<Executor, IConversationListener> listener = listenersToProxy.get( listenerForConversation1); ConversationChannel conversation = getConversation(CONVERSATION_ID_1); listener.second.onConversationUpdate(getConversation(CONVERSATION_ID_1)); mTestLooper.dispatchAll(); // Only call the associated listener. verify(listenerForConversation2, never()).onConversationUpdate(any()); // Should update the listeners mapped to the proxy. ArgumentCaptor<ConversationChannel> capturedConversation = ArgumentCaptor.forClass( ConversationChannel.class); verify(listenerForConversation1, times(1)).onConversationUpdate( capturedConversation.capture()); ConversationChannel conversationChannel = capturedConversation.getValue(); assertEquals(conversationChannel.getShortcutInfo().getId(), CONVERSATION_ID_1); assertEquals(conversationChannel.getShortcutInfo().getLabel(), conversation.getShortcutInfo().getLabel()); } private ConversationChannel getConversation(String shortcutId) { ShortcutInfo shortcutInfo = new ShortcutInfo.Builder(mContext, shortcutId).setLongLabel( "name").build(); NotificationChannel notificationChannel = new NotificationChannel("123", "channel", NotificationManager.IMPORTANCE_DEFAULT); return new ConversationChannel(shortcutInfo, 0, notificationChannel, null, 123L, false); } private void registerListener(String conversationId, PeopleManager.ConversationListener listener) { mPeopleManager.registerConversationListener(mContext.getPackageName(), mContext.getUserId(), conversationId, listener, mTestLooper.getNewExecutor()); } }
services/people/java/com/android/server/people/PeopleService.java +141 −2 Original line number Diff line number Diff line Loading @@ -23,6 +23,7 @@ import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.people.ConversationChannel; import android.app.people.ConversationStatus; import android.app.people.IConversationListener; import android.app.people.IPeopleManager; import android.app.prediction.AppPredictionContext; import android.app.prediction.AppPredictionSessionId; Loading @@ -33,10 +34,12 @@ import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ParceledListSlice; import android.content.pm.ShortcutInfo; import android.os.Binder; import android.os.CancellationSignal; import android.os.IBinder; import android.os.Process; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.UserHandle; import android.util.ArrayMap; Loading @@ -47,6 +50,7 @@ import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.people.data.DataManager; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; Loading @@ -58,7 +62,9 @@ public class PeopleService extends SystemService { private static final String TAG = "PeopleService"; private final DataManager mDataManager; private DataManager mDataManager; @VisibleForTesting ConversationListenerHelper mConversationListenerHelper; private PackageManagerInternal mPackageManagerInternal; Loading @@ -71,6 +77,8 @@ public class PeopleService extends SystemService { super(context); mDataManager = new DataManager(context); mConversationListenerHelper = new ConversationListenerHelper(); mDataManager.addConversationsListener(mConversationListenerHelper); } @Override Loading Loading @@ -148,12 +156,14 @@ public class PeopleService extends SystemService { * @param message used as message if SecurityException is thrown * @throws SecurityException if the caller is not system or root */ private static void enforceSystemRootOrSystemUI(Context context, String message) { @VisibleForTesting protected void enforceSystemRootOrSystemUI(Context context, String message) { if (isSystemOrRoot()) return; context.enforceCallingPermission(android.Manifest.permission.STATUS_BAR_SERVICE, message); } @VisibleForTesting final IBinder mService = new IPeopleManager.Stub() { @Override Loading Loading @@ -241,8 +251,137 @@ public class PeopleService extends SystemService { return new ParceledListSlice<>( mDataManager.getStatuses(packageName, userId, conversationId)); } @Override public void registerConversationListener( String packageName, int userId, String shortcutId, IConversationListener listener) { enforceSystemRootOrSystemUI(getContext(), "register conversation listener"); mConversationListenerHelper.addConversationListener( new ListenerKey(packageName, userId, shortcutId), listener); } @Override public void unregisterConversationListener(IConversationListener listener) { enforceSystemRootOrSystemUI(getContext(), "unregister conversation listener"); mConversationListenerHelper.removeConversationListener(listener); } }; /** * Listeners for conversation changes. * * @hide */ public interface ConversationsListener { /** * Triggers with the list of modified conversations from {@link DataManager} for dispatching * relevant updates to clients. * * @param conversations The conversations with modified data * @see IPeopleManager#registerConversationListener(String, int, String, * android.app.people.ConversationListener) */ default void onConversationsUpdate(@NonNull List<ConversationChannel> conversations) { } } /** * Implements {@code ConversationListenerHelper} to dispatch conversation updates to registered * clients. */ public static class ConversationListenerHelper implements ConversationsListener { ConversationListenerHelper() { } @VisibleForTesting final RemoteCallbackList<IConversationListener> mListeners = new RemoteCallbackList<>(); /** Adds {@code listener} with {@code key} associated. */ public synchronized void addConversationListener(ListenerKey key, IConversationListener listener) { mListeners.unregister(listener); mListeners.register(listener, key); } /** Removes {@code listener}. */ public synchronized void removeConversationListener( IConversationListener listener) { mListeners.unregister(listener); } @Override /** Dispatches updates to {@code mListeners} with keys mapped to {@code conversations}. */ public void onConversationsUpdate(List<ConversationChannel> conversations) { int count = mListeners.beginBroadcast(); // Early opt-out if no listeners are registered. if (count == 0) { return; } Map<ListenerKey, ConversationChannel> keyedConversations = new HashMap<>(); for (ConversationChannel conversation : conversations) { keyedConversations.put(getListenerKey(conversation), conversation); } for (int i = 0; i < count; i++) { final ListenerKey listenerKey = (ListenerKey) mListeners.getBroadcastCookie(i); if (!keyedConversations.containsKey(listenerKey)) { continue; } final IConversationListener listener = mListeners.getBroadcastItem(i); try { ConversationChannel channel = keyedConversations.get(listenerKey); listener.onConversationUpdate(channel); } catch (RemoteException e) { // The RemoteCallbackList will take care of removing the dead object. } } mListeners.finishBroadcast(); } private ListenerKey getListenerKey(ConversationChannel conversation) { ShortcutInfo info = conversation.getShortcutInfo(); return new ListenerKey(info.getPackage(), info.getUserId(), info.getId()); } } private static class ListenerKey { private final String mPackageName; private final Integer mUserId; private final String mShortcutId; ListenerKey(String packageName, Integer userId, String shortcutId) { this.mPackageName = packageName; this.mUserId = userId; this.mShortcutId = shortcutId; } public String getPackageName() { return mPackageName; } public Integer getUserId() { return mUserId; } public String getShortcutId() { return mShortcutId; } @Override public boolean equals(Object o) { ListenerKey key = (ListenerKey) o; return key.getPackageName().equals(mPackageName) && key.getUserId() == mUserId && key.getShortcutId().equals(mShortcutId); } @Override public int hashCode() { return mPackageName.hashCode() + mUserId.hashCode() + mShortcutId.hashCode(); } } @VisibleForTesting final class LocalService extends PeopleServiceInternal { Loading