From 20b32edbfe3b1b3d2f1a56bb73c4a903ca7b3feb Mon Sep 17 00:00:00 2001 From: Danning Chen Date: Wed, 18 Dec 2019 16:52:03 -0800 Subject: [PATCH] Add the skeleton code for People Service which is a new system service with only internal APIs Change-Id: I78b1b362735d5da40c49a344ae3063ea0df0759f Bug: 146522621 Test: Code builds and runs successfully --- services/Android.bp | 1 + .../server/people/PeopleServiceInternal.java | 24 +++ .../java/com/android/server/SystemServer.java | 5 + services/people/Android.bp | 5 + .../android/server/people/PeopleService.java | 141 ++++++++++++++++++ .../android/server/people/SessionInfo.java | 72 +++++++++ .../prediction/ConversationPredictor.java | 85 +++++++++++ services/tests/servicestests/Android.bp | 1 + .../server/people/PeopleServiceTest.java | 111 ++++++++++++++ 9 files changed, 445 insertions(+) create mode 100644 services/core/java/com/android/server/people/PeopleServiceInternal.java create mode 100644 services/people/Android.bp create mode 100644 services/people/java/com/android/server/people/PeopleService.java create mode 100644 services/people/java/com/android/server/people/SessionInfo.java create mode 100644 services/people/java/com/android/server/people/prediction/ConversationPredictor.java create mode 100644 services/tests/servicestests/src/com/android/server/people/PeopleServiceTest.java diff --git a/services/Android.bp b/services/Android.bp index ede4c3e7a6beb..1e119363f5458 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -61,6 +61,7 @@ java_library { "services.devicepolicy", "services.midi", "services.net", + "services.people", "services.print", "services.restrictions", "services.startop", diff --git a/services/core/java/com/android/server/people/PeopleServiceInternal.java b/services/core/java/com/android/server/people/PeopleServiceInternal.java new file mode 100644 index 0000000000000..31d303621116a --- /dev/null +++ b/services/core/java/com/android/server/people/PeopleServiceInternal.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2019 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 com.android.server.people; + +import android.service.appprediction.IPredictionService; + +/** + * @hide Only for use within the system server. + */ +public abstract class PeopleServiceInternal extends IPredictionService.Stub {} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index cfe1318999871..dd49be01a2ed9 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -126,6 +126,7 @@ import com.android.server.om.OverlayManagerService; import com.android.server.os.BugreportManagerService; import com.android.server.os.DeviceIdentifiersPolicyService; import com.android.server.os.SchedulingPolicyService; +import com.android.server.people.PeopleService; import com.android.server.pm.BackgroundDexOptService; import com.android.server.pm.CrossProfileAppsService; import com.android.server.pm.DataLoaderManagerService; @@ -1915,6 +1916,10 @@ public final class SystemServer { t.traceBegin("StartCrossProfileAppsService"); mSystemServiceManager.startService(CrossProfileAppsService.class); t.traceEnd(); + + t.traceBegin("StartPeopleService"); + mSystemServiceManager.startService(PeopleService.class); + t.traceEnd(); } if (!isWatch) { diff --git a/services/people/Android.bp b/services/people/Android.bp new file mode 100644 index 0000000000000..d64097a03ae9c --- /dev/null +++ b/services/people/Android.bp @@ -0,0 +1,5 @@ +java_library_static { + name: "services.people", + srcs: ["java/**/*.java"], + libs: ["services.core"], +} diff --git a/services/people/java/com/android/server/people/PeopleService.java b/services/people/java/com/android/server/people/PeopleService.java new file mode 100644 index 0000000000000..ef3da6085fb1a --- /dev/null +++ b/services/people/java/com/android/server/people/PeopleService.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2019 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 com.android.server.people; + +import android.app.prediction.AppPredictionContext; +import android.app.prediction.AppPredictionSessionId; +import android.app.prediction.AppTarget; +import android.app.prediction.AppTargetEvent; +import android.app.prediction.IPredictionCallback; +import android.content.Context; +import android.content.pm.ParceledListSlice; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.SystemService; + +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * A service that manages the people and conversations provided by apps. + */ +public class PeopleService extends SystemService { + + private static final String TAG = "PeopleService"; + + /** + * Initializes the system service. + * + * @param context The system server context. + */ + public PeopleService(Context context) { + super(context); + } + + @Override + public void onStart() { + publishLocalService(PeopleServiceInternal.class, new LocalService()); + } + + @VisibleForTesting + final class LocalService extends PeopleServiceInternal { + + private Map mSessions = new ArrayMap<>(); + + @Override + public void onCreatePredictionSession(AppPredictionContext context, + AppPredictionSessionId sessionId) { + mSessions.put(sessionId, new SessionInfo(context)); + } + + @Override + public void notifyAppTargetEvent(AppPredictionSessionId sessionId, AppTargetEvent event) { + runForSession(sessionId, + sessionInfo -> sessionInfo.getPredictor().onAppTargetEvent(event)); + } + + @Override + public void notifyLaunchLocationShown(AppPredictionSessionId sessionId, + String launchLocation, ParceledListSlice targetIds) { + runForSession(sessionId, + sessionInfo -> sessionInfo.getPredictor().onLaunchLocationShown( + launchLocation, targetIds.getList())); + } + + @Override + public void sortAppTargets(AppPredictionSessionId sessionId, ParceledListSlice targets, + IPredictionCallback callback) { + runForSession(sessionId, + sessionInfo -> sessionInfo.getPredictor().onSortAppTargets( + targets.getList(), + targetList -> invokePredictionCallback(callback, targetList))); + } + + @Override + public void registerPredictionUpdates(AppPredictionSessionId sessionId, + IPredictionCallback callback) { + runForSession(sessionId, sessionInfo -> sessionInfo.addCallback(callback)); + } + + @Override + public void unregisterPredictionUpdates(AppPredictionSessionId sessionId, + IPredictionCallback callback) { + runForSession(sessionId, sessionInfo -> sessionInfo.removeCallback(callback)); + } + + @Override + public void requestPredictionUpdate(AppPredictionSessionId sessionId) { + runForSession(sessionId, + sessionInfo -> sessionInfo.getPredictor().onRequestPredictionUpdate()); + } + + @Override + public void onDestroyPredictionSession(AppPredictionSessionId sessionId) { + runForSession(sessionId, sessionInfo -> { + sessionInfo.onDestroy(); + mSessions.remove(sessionId); + }); + } + + @VisibleForTesting + SessionInfo getSessionInfo(AppPredictionSessionId sessionId) { + return mSessions.get(sessionId); + } + + private void runForSession(AppPredictionSessionId sessionId, Consumer method) { + SessionInfo sessionInfo = mSessions.get(sessionId); + if (sessionInfo == null) { + Slog.e(TAG, "Failed to find the session: " + sessionId); + return; + } + method.accept(sessionInfo); + } + + private void invokePredictionCallback(IPredictionCallback callback, + List targets) { + try { + callback.onResult(new ParceledListSlice<>(targets)); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to calling callback" + e); + } + } + } +} diff --git a/services/people/java/com/android/server/people/SessionInfo.java b/services/people/java/com/android/server/people/SessionInfo.java new file mode 100644 index 0000000000000..df7cedf7626ff --- /dev/null +++ b/services/people/java/com/android/server/people/SessionInfo.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2019 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 com.android.server.people; + +import android.app.prediction.AppPredictionContext; +import android.app.prediction.AppTarget; +import android.app.prediction.IPredictionCallback; +import android.content.pm.ParceledListSlice; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.server.people.prediction.ConversationPredictor; + +import java.util.List; + +/** Manages the information and callbacks in an app prediction request session. */ +class SessionInfo { + + private static final String TAG = "SessionInfo"; + + private final ConversationPredictor mConversationPredictor; + private final RemoteCallbackList mCallbacks = + new RemoteCallbackList<>(); + + SessionInfo(AppPredictionContext predictionContext) { + mConversationPredictor = new ConversationPredictor(predictionContext, + this::updatePredictions); + } + + void addCallback(IPredictionCallback callback) { + mCallbacks.register(callback); + } + + void removeCallback(IPredictionCallback callback) { + mCallbacks.unregister(callback); + } + + ConversationPredictor getPredictor() { + return mConversationPredictor; + } + + void onDestroy() { + mCallbacks.kill(); + } + + private void updatePredictions(List targets) { + int callbackCount = mCallbacks.beginBroadcast(); + for (int i = 0; i < callbackCount; i++) { + try { + mCallbacks.getBroadcastItem(i).onResult(new ParceledListSlice<>(targets)); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to calling callback" + e); + } + } + mCallbacks.finishBroadcast(); + } +} diff --git a/services/people/java/com/android/server/people/prediction/ConversationPredictor.java b/services/people/java/com/android/server/people/prediction/ConversationPredictor.java new file mode 100644 index 0000000000000..de71d292ff7dc --- /dev/null +++ b/services/people/java/com/android/server/people/prediction/ConversationPredictor.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2019 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 com.android.server.people.prediction; + +import android.annotation.MainThread; +import android.app.prediction.AppPredictionContext; +import android.app.prediction.AppTarget; +import android.app.prediction.AppTargetEvent; +import android.app.prediction.AppTargetId; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + +/** + * Predictor that predicts the conversations or apps the user is most likely to open. + */ +public class ConversationPredictor { + + private final AppPredictionContext mPredictionContext; + private final Consumer> mUpdatePredictionsMethod; + private final ExecutorService mCallbackExecutor; + + public ConversationPredictor(AppPredictionContext predictionContext, + Consumer> updatePredictionsMethod) { + mPredictionContext = predictionContext; + mUpdatePredictionsMethod = updatePredictionsMethod; + mCallbackExecutor = Executors.newSingleThreadExecutor(); + } + + /** + * Called by the client app to indicate a target launch. + */ + @MainThread + public void onAppTargetEvent(AppTargetEvent event) { + } + + /** + * Called by the client app to indicate a particular location has been shown to the user. + */ + @MainThread + public void onLaunchLocationShown(String launchLocation, List targetIds) { + } + + /** + * Called by the client app to request sorting of the provided targets based on the prediction + * ranking. + */ + @MainThread + public void onSortAppTargets(List targets, Consumer> callback) { + mCallbackExecutor.execute(() -> callback.accept(targets)); + } + + /** + * Called by the client app to request target predictions. + */ + @MainThread + public void onRequestPredictionUpdate() { + List targets = new ArrayList<>(); + mCallbackExecutor.execute(() -> mUpdatePredictionsMethod.accept(targets)); + } + + @VisibleForTesting + public Consumer> getUpdatePredictionsMethod() { + return mUpdatePredictionsMethod; + } +} diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index 015e574f2309f..48cffa08f8de2 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -26,6 +26,7 @@ android_test { "services.core", "services.devicepolicy", "services.net", + "services.people", "services.usage", "guava", "androidx.test.core", diff --git a/services/tests/servicestests/src/com/android/server/people/PeopleServiceTest.java b/services/tests/servicestests/src/com/android/server/people/PeopleServiceTest.java new file mode 100644 index 0000000000000..d3166b91dc9e7 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/people/PeopleServiceTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2019 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 com.android.server.people; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +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.os.Binder; +import android.os.RemoteException; + +import com.android.server.LocalServices; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +@RunWith(JUnit4.class) +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 PeopleServiceInternal mServiceInternal; + private PeopleService.LocalService mLocalService; + private AppPredictionSessionId mSessionId; + private AppPredictionContext mPredictionContext; + + @Mock private Context mContext; + @Mock private IPredictionCallback mCallback; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + when(mContext.getPackageName()).thenReturn(TEST_PACKAGE_NAME); + when(mCallback.asBinder()).thenReturn(new Binder()); + + PeopleService service = new PeopleService(mContext); + service.onStart(); + + mServiceInternal = LocalServices.getService(PeopleServiceInternal.class); + mLocalService = (PeopleService.LocalService) mServiceInternal; + + mSessionId = new AppPredictionSessionId("abc"); + mPredictionContext = new AppPredictionContext.Builder(mContext) + .setUiSurface(APP_PREDICTION_SHARE_UI_SURFACE) + .setPredictedTargetCount(APP_PREDICTION_TARGET_COUNT) + .build(); + } + + @After + public void tearDown() { + LocalServices.removeServiceForTest(PeopleServiceInternal.class); + } + + @Test + public void testRegisterCallbacks() throws RemoteException { + mServiceInternal.onCreatePredictionSession(mPredictionContext, mSessionId); + + SessionInfo sessionInfo = mLocalService.getSessionInfo(mSessionId); + + mServiceInternal.registerPredictionUpdates(mSessionId, mCallback); + + Consumer> updatePredictionMethod = + sessionInfo.getPredictor().getUpdatePredictionsMethod(); + updatePredictionMethod.accept(new ArrayList<>()); + updatePredictionMethod.accept(new ArrayList<>()); + + verify(mCallback, times(2)).onResult(any(ParceledListSlice.class)); + + mServiceInternal.unregisterPredictionUpdates(mSessionId, mCallback); + + updatePredictionMethod.accept(new ArrayList<>()); + + // After the un-registration, the callback should no longer be called. + verify(mCallback, times(2)).onResult(any(ParceledListSlice.class)); + + mServiceInternal.onDestroyPredictionSession(mSessionId); + } +}