Merge "Add listener for conversation changes" into sc-dev

This commit is contained in:
TreeHugger Robot
2021-02-17 23:53:55 +00:00
committed by Android (Google) Code Review
8 changed files with 748 additions and 32 deletions

View File

@@ -0,0 +1,33 @@
/**
* 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);
}

View File

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

View File

@@ -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;
@@ -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.
@@ -40,11 +48,18 @@ public final class PeopleManager {
private static final String LOG_TAG = PeopleManager.class.getSimpleName();
@NonNull
private final Context mContext;
/**
* @hide
*/
@VisibleForTesting
public Map<ConversationListener, Pair<Executor, IConversationListener>>
mConversationListeners = new HashMap<>();
@NonNull
private final IPeopleManager mService;
private Context mContext;
@NonNull
private IPeopleManager mService;
/**
* @hide
@@ -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.
*
@@ -66,9 +90,8 @@ public final class PeopleManager {
* clients.
*
* @param packageName name of the package the conversation is part of
* @param shortcutId the shortcut id backing the conversation
* @param shortcutId the shortcut id backing the conversation
* @return whether the {@shortcutId} is backed by a Conversation.
*
* @hide
*/
@SystemApi
@@ -94,8 +117,7 @@ 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
*
* @param status the current status for the given conversation
* @return whether the role is available in the system
*/
public void addOrUpdateStatus(@NonNull String conversationId,
@@ -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);
@@ -155,7 +177,7 @@ public final class PeopleManager {
try {
final ParceledListSlice<ConversationStatus> parceledList
= mService.getStatuses(
mContext.getPackageName(), mContext.getUserId(), conversationId);
mContext.getPackageName(), mContext.getUserId(), conversationId);
if (parceledList != null) {
return parceledList.getList();
}
@@ -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));
}
}
}

View File

@@ -0,0 +1,120 @@
/*
* 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());
}
}

View File

@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -71,6 +77,8 @@ public class PeopleService extends SystemService {
super(context);
mDataManager = new DataManager(context);
mConversationListenerHelper = new ConversationListenerHelper();
mDataManager.addConversationsListener(mConversationListenerHelper);
}
@Override
@@ -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
@@ -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 {

View File

@@ -48,6 +48,7 @@ import android.database.ContentObserver;
import android.net.Uri;
import android.os.CancellationSignal;
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
@@ -75,14 +76,17 @@ import com.android.internal.telephony.SmsApplication;
import com.android.server.LocalServices;
import com.android.server.notification.NotificationManagerInternal;
import com.android.server.notification.ShortcutHelper;
import com.android.server.people.PeopleService;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.concurrent.Executor;
@@ -118,6 +122,11 @@ public class DataManager {
private final SparseArray<ScheduledFuture<?>> mUsageStatsQueryFutures = new SparseArray<>();
private final SparseArray<NotificationListener> mNotificationListeners = new SparseArray<>();
private final SparseArray<PackageMonitor> mPackageMonitors = new SparseArray<>();
@GuardedBy("mLock")
private final List<PeopleService.ConversationsListener> mConversationsListeners =
new ArrayList<>(1);
private final Handler mHandler;
private ContentObserver mCallLogContentObserver;
private ContentObserver mMmsSmsContentObserver;
@@ -128,14 +137,14 @@ public class DataManager {
private ConversationStatusExpirationBroadcastReceiver mStatusExpReceiver;
public DataManager(Context context) {
this(context, new Injector());
this(context, new Injector(), BackgroundThread.get().getLooper());
}
@VisibleForTesting
DataManager(Context context, Injector injector) {
DataManager(Context context, Injector injector, Looper looper) {
mContext = context;
mInjector = injector;
mScheduledExecutor = mInjector.createScheduledExecutor();
mHandler = new Handler(looper);
}
/** Initialization. Called when the system services are up running. */
@@ -234,20 +243,19 @@ public class DataManager {
PackageData packageData = userData.getPackageData(packageName);
// App may have been uninstalled.
if (packageData != null) {
return getConversationChannel(packageData, shortcutId);
ConversationInfo conversationInfo = packageData.getConversationInfo(shortcutId);
return getConversationChannel(packageName, userId, shortcutId, conversationInfo);
}
}
return null;
}
@Nullable
private ConversationChannel getConversationChannel(PackageData packageData, String shortcutId) {
ConversationInfo conversationInfo = packageData.getConversationInfo(shortcutId);
private ConversationChannel getConversationChannel(String packageName, int userId,
String shortcutId, ConversationInfo conversationInfo) {
if (conversationInfo == null) {
return null;
}
int userId = packageData.getUserId();
String packageName = packageData.getPackageName();
ShortcutInfo shortcutInfo = getShortcut(packageName, userId, shortcutId);
if (shortcutInfo == null) {
return null;
@@ -278,7 +286,8 @@ public class DataManager {
return;
}
String shortcutId = conversationInfo.getShortcutId();
ConversationChannel channel = getConversationChannel(packageData, shortcutId);
ConversationChannel channel = getConversationChannel(packageData.getPackageName(),
packageData.getUserId(), shortcutId, conversationInfo);
if (channel == null || channel.getParentNotificationChannel() == null) {
return;
}
@@ -384,7 +393,11 @@ public class DataManager {
ConversationInfo convToModify = getConversationInfoOrThrow(cs, conversationId);
ConversationInfo.Builder builder = new ConversationInfo.Builder(convToModify);
builder.addOrUpdateStatus(status);
cs.addOrUpdate(builder.build());
ConversationInfo modifiedConv = builder.build();
cs.addOrUpdate(modifiedConv);
ConversationChannel conversation = getConversationChannel(packageName, userId,
conversationId, modifiedConv);
notifyConversationsListeners(Arrays.asList(conversation));
if (status.getEndTimeMillis() >= 0) {
mStatusExpReceiver.scheduleExpiration(
@@ -1235,6 +1248,32 @@ public class DataManager {
}
}
/** Adds {@code listener} to be notified on conversation changes. */
public void addConversationsListener(
@NonNull PeopleService.ConversationsListener listener) {
synchronized (mConversationsListeners) {
mConversationsListeners.add(Objects.requireNonNull(listener));
}
}
// TODO(b/178792356): Trigger ConversationsListener on all-related data changes.
@VisibleForTesting
void notifyConversationsListeners(
@Nullable final List<ConversationChannel> changedConversations) {
mHandler.post(() -> {
try {
final List<PeopleService.ConversationsListener> copy;
synchronized (mLock) {
copy = new ArrayList<>(mConversationsListeners);
}
for (PeopleService.ConversationsListener listener : copy) {
listener.onConversationsUpdate(changedConversations);
}
} catch (Exception e) {
}
});
}
/** A {@link BroadcastReceiver} that receives the intents for a specified user. */
private class PerUserBroadcastReceiver extends BroadcastReceiver {

View File

@@ -16,58 +16,104 @@
package com.android.server.people;
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.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
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 static org.mockito.Mockito.when;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.people.ConversationChannel;
import android.app.people.ConversationStatus;
import android.app.people.IConversationListener;
import android.app.people.IPeopleManager;
import android.app.people.PeopleManager;
import android.app.prediction.AppPredictionContext;
import android.app.prediction.AppPredictionSessionId;
import android.app.prediction.AppTarget;
import android.app.prediction.IPredictionCallback;
import android.content.Context;
import android.content.pm.ParceledListSlice;
import android.content.pm.ShortcutInfo;
import android.os.Binder;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.test.TestLooper;
import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
import android.testing.TestableLooper;
import androidx.test.InstrumentationRegistry;
import com.android.server.LocalServices;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
@RunWith(JUnit4.class)
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public final class PeopleServiceTest {
private static final String APP_PREDICTION_SHARE_UI_SURFACE = "share";
private static final int APP_PREDICTION_TARGET_COUNT = 4;
private static final String TEST_PACKAGE_NAME = "com.example";
private static final int USER_ID = 0;
private static final String CONVERSATION_ID_1 = "12";
private static final String CONVERSATION_ID_2 = "123";
private PeopleServiceInternal mServiceInternal;
private PeopleService.LocalService mLocalService;
private AppPredictionSessionId mSessionId;
private AppPredictionContext mPredictionContext;
@Mock private Context mContext;
@Mock private IPredictionCallback mCallback;
@Mock
private Context mMockContext;
@Rule
public final TestableContext mContext =
new TestableContext(InstrumentationRegistry.getContext(), null);
protected TestableContext getContext() {
return mContext;
}
@Mock
private IPredictionCallback mCallback;
private TestableLooper mTestableLooper;
private final TestLooper mTestLooper = new TestLooper();
private TestablePeopleService mPeopleService;
private IPeopleManager mIPeopleManager;
private PeopleManager mPeopleManager;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
when(mContext.getPackageName()).thenReturn(TEST_PACKAGE_NAME);
mPeopleService = new TestablePeopleService(mContext);
mTestableLooper = TestableLooper.get(this);
mIPeopleManager = ((IPeopleManager) mPeopleService.mService);
mPeopleManager = new PeopleManager(mContext, mIPeopleManager);
when(mMockContext.getPackageName()).thenReturn(TEST_PACKAGE_NAME);
when(mCallback.asBinder()).thenReturn(new Binder());
PeopleService service = new PeopleService(mContext);
service.onStart(/* isForTesting= */ true);
@@ -75,7 +121,7 @@ public final class PeopleServiceTest {
mLocalService = (PeopleService.LocalService) mServiceInternal;
mSessionId = new AppPredictionSessionId("abc", USER_ID);
mPredictionContext = new AppPredictionContext.Builder(mContext)
mPredictionContext = new AppPredictionContext.Builder(mMockContext)
.setUiSurface(APP_PREDICTION_SHARE_UI_SURFACE)
.setPredictedTargetCount(APP_PREDICTION_TARGET_COUNT)
.setExtras(new Bundle())
@@ -111,4 +157,134 @@ public final class PeopleServiceTest {
mServiceInternal.onDestroyPredictionSession(mSessionId);
}
@Test
public void testRegisterConversationListener() throws Exception {
assertEquals(0,
mPeopleService.mConversationListenerHelper.mListeners.getRegisteredCallbackCount());
mIPeopleManager.registerConversationListener(TEST_PACKAGE_NAME, 0, CONVERSATION_ID_1,
new TestableConversationListener());
mTestableLooper.processAllMessages();
assertEquals(1,
mPeopleService.mConversationListenerHelper.mListeners.getRegisteredCallbackCount());
mIPeopleManager.registerConversationListener(TEST_PACKAGE_NAME, 0, CONVERSATION_ID_1,
new TestableConversationListener());
mTestableLooper.processAllMessages();
assertEquals(2,
mPeopleService.mConversationListenerHelper.mListeners.getRegisteredCallbackCount());
mIPeopleManager.registerConversationListener(TEST_PACKAGE_NAME, 0, CONVERSATION_ID_2,
new TestableConversationListener());
mTestableLooper.processAllMessages();
assertEquals(3,
mPeopleService.mConversationListenerHelper.mListeners.getRegisteredCallbackCount());
}
@Test
public void testUnregisterConversationListener() throws Exception {
TestableConversationListener listener1 = new TestableConversationListener();
mIPeopleManager.registerConversationListener(TEST_PACKAGE_NAME, 0, CONVERSATION_ID_1,
listener1);
TestableConversationListener listener2 = new TestableConversationListener();
mIPeopleManager.registerConversationListener(TEST_PACKAGE_NAME, 0, CONVERSATION_ID_1,
listener2);
TestableConversationListener listener3 = new TestableConversationListener();
mIPeopleManager.registerConversationListener(TEST_PACKAGE_NAME, 0, CONVERSATION_ID_2,
listener3);
mTestableLooper.processAllMessages();
assertEquals(3,
mPeopleService.mConversationListenerHelper.mListeners.getRegisteredCallbackCount());
mIPeopleManager.unregisterConversationListener(
listener2);
assertEquals(2,
mPeopleService.mConversationListenerHelper.mListeners.getRegisteredCallbackCount());
mIPeopleManager.unregisterConversationListener(
listener1);
assertEquals(1,
mPeopleService.mConversationListenerHelper.mListeners.getRegisteredCallbackCount());
mIPeopleManager.unregisterConversationListener(
listener3);
assertEquals(0,
mPeopleService.mConversationListenerHelper.mListeners.getRegisteredCallbackCount());
}
@Test
public void testOnlyTriggersConversationListenersForRegisteredConversation() {
PeopleManager.ConversationListener listenerForConversation1 = mock(
PeopleManager.ConversationListener.class);
registerListener(CONVERSATION_ID_1, listenerForConversation1);
PeopleManager.ConversationListener secondListenerForConversation1 = mock(
PeopleManager.ConversationListener.class);
registerListener(CONVERSATION_ID_1, secondListenerForConversation1);
PeopleManager.ConversationListener listenerForConversation2 = mock(
PeopleManager.ConversationListener.class);
registerListener(CONVERSATION_ID_2, listenerForConversation2);
assertEquals(3,
mPeopleService.mConversationListenerHelper.mListeners.getRegisteredCallbackCount());
// Update conversation with two listeners.
ConversationStatus status = new ConversationStatus.Builder(CONVERSATION_ID_1,
ACTIVITY_GAME).build();
mPeopleService.mConversationListenerHelper.onConversationsUpdate(
Arrays.asList(getConversation(CONVERSATION_ID_1, status)));
mTestLooper.dispatchAll();
// Never update listeners for other conversations.
verify(listenerForConversation2, never()).onConversationUpdate(any());
// Should update both listeners for the conversation.
ArgumentCaptor<ConversationChannel> capturedConversation = ArgumentCaptor.forClass(
ConversationChannel.class);
verify(listenerForConversation1, times(1)).onConversationUpdate(
capturedConversation.capture());
ConversationChannel conversationChannel = capturedConversation.getValue();
verify(secondListenerForConversation1, times(1)).onConversationUpdate(
eq(conversationChannel));
assertEquals(conversationChannel.getShortcutInfo().getId(), CONVERSATION_ID_1);
assertThat(conversationChannel.getStatuses()).containsExactly(status);
}
private void registerListener(String conversationId,
PeopleManager.ConversationListener listener) {
mPeopleManager.registerConversationListener(mContext.getPackageName(), mContext.getUserId(),
conversationId, listener,
mTestLooper.getNewExecutor());
}
private ConversationChannel getConversation(String shortcutId, ConversationStatus status) {
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, false, Arrays.asList(status));
}
private class TestableConversationListener extends IConversationListener.Stub {
@Override
public void onConversationUpdate(ConversationChannel conversation) {
}
}
// Use a Testable subclass so we can simulate calls from the system without failing.
private static class TestablePeopleService extends PeopleService {
TestablePeopleService(Context context) {
super(context);
}
@Override
public void onStart() {
super.onStart(true);
}
@Override
protected void enforceSystemRootOrSystemUI(Context context, String message) {
return;
}
}
}

View File

@@ -79,6 +79,7 @@ import android.os.CancellationSignal;
import android.os.Looper;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.test.TestLooper;
import android.provider.ContactsContract;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
@@ -91,8 +92,11 @@ import com.android.internal.app.ChooserActivity;
import com.android.internal.content.PackageMonitor;
import com.android.server.LocalServices;
import com.android.server.notification.NotificationManagerInternal;
import com.android.server.people.PeopleService;
import com.android.server.pm.parsing.pkg.AndroidPackage;
import com.google.common.collect.Iterables;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -123,6 +127,7 @@ public final class DataManagerTest {
private static final String TEST_PKG_NAME = "pkg";
private static final String TEST_CLASS_NAME = "class";
private static final String TEST_SHORTCUT_ID = "sc";
private static final String TEST_SHORTCUT_ID_2 = "sc2";
private static final int TEST_PKG_UID = 35;
private static final String CONTACT_URI = "content://com.android.contacts/contacts/lookup/123";
private static final String PHONE_NUMBER = "+1234567890";
@@ -157,6 +162,7 @@ public final class DataManagerTest {
private ShortcutChangeCallback mShortcutChangeCallback;
private ShortcutInfo mShortcutInfo;
private TestInjector mInjector;
private TestLooper mLooper;
@Before
public void setUp() throws PackageManager.NameNotFoundException {
@@ -237,7 +243,8 @@ public final class DataManagerTest {
mCancellationSignal = new CancellationSignal();
mInjector = new TestInjector();
mDataManager = new DataManager(mContext, mInjector);
mLooper = new TestLooper();
mDataManager = new DataManager(mContext, mInjector, mLooper.getLooper());
mDataManager.initialize();
when(mShortcutServiceInternal.isSharingShortcut(anyInt(), anyString(), anyString(),
@@ -514,6 +521,84 @@ public final class DataManagerTest {
assertTrue(mDataManager.getRecentConversations(USER_ID_PRIMARY).isEmpty());
}
@Test
public void testAddConversationsListener() throws Exception {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
buildPerson());
mDataManager.addOrUpdateConversationInfo(shortcut);
ConversationChannel conversationChannel = mDataManager.getConversation(TEST_PKG_NAME,
USER_ID_PRIMARY,
TEST_SHORTCUT_ID);
PeopleService.ConversationsListener listener = mock(
PeopleService.ConversationsListener.class);
mDataManager.addConversationsListener(listener);
List<ConversationChannel> changedConversations = Arrays.asList(conversationChannel);
verify(listener, times(0)).onConversationsUpdate(eq(changedConversations));
mDataManager.notifyConversationsListeners(changedConversations);
mLooper.dispatchAll();
verify(listener, times(1)).onConversationsUpdate(eq(changedConversations));
}
@Test
public void testAddConversationListenersNotifiesMultipleConversations() throws Exception {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
buildPerson());
mDataManager.addOrUpdateConversationInfo(shortcut);
ConversationChannel conversationChannel = mDataManager.getConversation(TEST_PKG_NAME,
USER_ID_PRIMARY,
TEST_SHORTCUT_ID);
ShortcutInfo shortcut2 = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY,
TEST_SHORTCUT_ID_2,
buildPerson());
mDataManager.addOrUpdateConversationInfo(shortcut2);
ConversationChannel conversationChannel2 = mDataManager.getConversation(TEST_PKG_NAME,
USER_ID_PRIMARY,
TEST_SHORTCUT_ID_2);
PeopleService.ConversationsListener listener = mock(
PeopleService.ConversationsListener.class);
mDataManager.addConversationsListener(listener);
List<ConversationChannel> changedConversations = Arrays.asList(conversationChannel,
conversationChannel2);
verify(listener, times(0)).onConversationsUpdate(eq(changedConversations));
mDataManager.notifyConversationsListeners(changedConversations);
mLooper.dispatchAll();
verify(listener, times(1)).onConversationsUpdate(eq(changedConversations));
ArgumentCaptor<List<ConversationChannel>> capturedConversation = ArgumentCaptor.forClass(
List.class);
verify(listener, times(1)).onConversationsUpdate(capturedConversation.capture());
assertThat(capturedConversation.getValue()).containsExactly(conversationChannel,
conversationChannel2);
}
@Test
public void testAddOrUpdateStatusNotifiesConversationsListeners() throws Exception {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
buildPerson());
mDataManager.addOrUpdateConversationInfo(shortcut);
PeopleService.ConversationsListener listener = mock(
PeopleService.ConversationsListener.class);
mDataManager.addConversationsListener(listener);
ConversationStatus status = new ConversationStatus.Builder("cs1", ACTIVITY_GAME).build();
mDataManager.addOrUpdateStatus(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID, status);
mLooper.dispatchAll();
ArgumentCaptor<List<ConversationChannel>> capturedConversation = ArgumentCaptor.forClass(
List.class);
verify(listener, times(1)).onConversationsUpdate(capturedConversation.capture());
ConversationChannel result = Iterables.getOnlyElement(capturedConversation.getValue());
assertThat(result.getStatuses()).containsExactly(status);
assertEquals(result.getShortcutInfo().getId(), TEST_SHORTCUT_ID);
}
@Test
public void testGetConversationReturnsCustomizedConversation() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
@@ -975,7 +1060,7 @@ public final class DataManagerTest {
mDataManager.reportShareTargetEvent(appTargetEvent, intentFilter);
byte[] payload = mDataManager.getBackupPayload(USER_ID_PRIMARY);
DataManager dataManager = new DataManager(mContext, mInjector);
DataManager dataManager = new DataManager(mContext, mInjector, mLooper.getLooper());
dataManager.onUserUnlocked(USER_ID_PRIMARY);
dataManager.restore(USER_ID_PRIMARY, payload);
ConversationInfo conversationInfo = dataManager.getPackage(TEST_PKG_NAME, USER_ID_PRIMARY)