diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 6d5f4542eb019..90506531ad449 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -2905,6 +2905,7 @@ package android.content.pm {
field public static final String EXTRA_REQUEST_PERMISSIONS_RESULTS = "android.content.pm.extra.REQUEST_PERMISSIONS_RESULTS";
field public static final String FEATURE_BROADCAST_RADIO = "android.hardware.broadcastradio";
field public static final String FEATURE_CONTEXT_HUB = "android.hardware.context_hub";
+ field public static final String FEATURE_GAME_SERVICE = "android.software.game_service";
field public static final String FEATURE_INCREMENTAL_DELIVERY = "android.software.incremental_delivery";
field public static final String FEATURE_REBOOT_ESCROW = "android.hardware.reboot_escrow";
field public static final String FEATURE_TELEPHONY_CARRIERLOCK = "android.hardware.telephony.carrierlock";
@@ -10458,6 +10459,18 @@ package android.service.euicc {
}
+package android.service.games {
+
+ public class GameService extends android.app.Service {
+ ctor public GameService();
+ method @Nullable public android.os.IBinder onBind(@Nullable android.content.Intent);
+ method public void onConnected();
+ method public void onDisconnected();
+ field public static final String SERVICE_INTERFACE = "android.service.games.GameService";
+ }
+
+}
+
package android.service.notification {
public final class Adjustment implements android.os.Parcelable {
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index a7f38016fb48e..191167067ddbb 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -3410,6 +3410,18 @@ public abstract class PackageManager {
@SdkConstant(SdkConstantType.FEATURE)
public static final String FEATURE_CANT_SAVE_STATE = "android.software.cant_save_state";
+ /**
+ * @hide
+ * Feature for {@link #getSystemAvailableFeatures} and
+ * {@link #hasSystemFeature}: The device supports
+ * {@link android.service.games.GameService}.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.FEATURE)
+ @SystemApi
+ public static final String FEATURE_GAME_SERVICE = "android.software.game_service";
+
/**
* @hide
* Feature for {@link #getSystemAvailableFeatures} and
diff --git a/core/java/android/service/games/GameService.java b/core/java/android/service/games/GameService.java
new file mode 100644
index 0000000000000..4b440ddf5405b
--- /dev/null
+++ b/core/java/android/service/games/GameService.java
@@ -0,0 +1,118 @@
+/*
+ * 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.service.games;
+
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SystemApi;
+import android.app.IGameManagerService;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import com.android.internal.util.function.pooled.PooledLambda;
+
+import java.util.Objects;
+
+/**
+ * Top-level service of the game service, which provides support for determining
+ * when a game session should begin. It is always kept running by the system.
+ * Because of this it should be kept as lightweight as possible.
+ *
+ * Heavy weight operations (such as showing UI) should be implemented in the
+ * associated {@link GameSessionService} when a game session is taking place. Its
+ * implementation should run in a separate process from the {@link GameService}.
+ *
+ * @hide
+ */
+@SystemApi
+public class GameService extends Service {
+ static final String TAG = "GameService";
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ * To be supported, the service must also require the
+ * {@link android.Manifest.permission#BIND_GAME_SERVICE} permission so
+ * that other applications can not abuse it.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE =
+ "android.service.games.GameService";
+
+ private IGameManagerService mGameManagerService;
+ private final IGameService mInterface = new IGameService.Stub() {
+ @Override
+ public void connected() {
+ Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage(
+ GameService::doOnConnected, GameService.this));
+ }
+
+ @Override
+ public void disconnected() {
+ Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage(
+ GameService::onDisconnected, GameService.this));
+ }
+ };
+ private final IBinder.DeathRecipient mGameManagerServiceDeathRecipient = () -> {
+ Log.w(TAG, "System service binder died. Shutting down");
+
+ Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage(
+ GameService::onDisconnected, GameService.this));
+ };
+
+ @Override
+ @Nullable
+ public IBinder onBind(@Nullable Intent intent) {
+ if (SERVICE_INTERFACE.equals(intent.getAction())) {
+ return mInterface.asBinder();
+ }
+ return null;
+ }
+
+ private void doOnConnected() {
+ mGameManagerService =
+ IGameManagerService.Stub.asInterface(
+ ServiceManager.getService(Context.GAME_SERVICE));
+ Objects.requireNonNull(mGameManagerService);
+ try {
+ mGameManagerService.asBinder().linkToDeath(mGameManagerServiceDeathRecipient, 0);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Unable to link to death with system service");
+ }
+
+ onConnected();
+ }
+
+ /**
+ * Called during service initialization to indicate that the system is ready
+ * to receive interaction from it. You should generally do initialization here
+ * rather than in {@link #onCreate}.
+ */
+ public void onConnected() {}
+
+ /**
+ * Called during service de-initialization to indicate that the system is shutting the
+ * service down. At this point this service may no longer be the active {@link GameService}.
+ * The service should clean up any resources that it holds at this point.
+ */
+ public void onDisconnected() {}
+}
diff --git a/core/java/android/service/games/IGameService.aidl b/core/java/android/service/games/IGameService.aidl
new file mode 100644
index 0000000000000..8a0d6365977bc
--- /dev/null
+++ b/core/java/android/service/games/IGameService.aidl
@@ -0,0 +1,25 @@
+/*
+ * 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.service.games;
+
+/**
+ * @hide
+ */
+oneway interface IGameService {
+ void connected();
+ void disconnected();
+}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 158201777bd51..29f13c91577b4 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3922,6 +3922,15 @@
+
+
+
+
+
+
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 2083d6566c9ce..ccb487e318018 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4624,4 +4624,6 @@
+
+
diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java
index 456a75832b161..20606f0e9f4d1 100644
--- a/services/core/java/com/android/server/app/GameManagerService.java
+++ b/services/core/java/com/android/server/app/GameManagerService.java
@@ -498,6 +498,8 @@ public final class GameManagerService extends IGameManagerService.Stub {
*/
public static class Lifecycle extends SystemService {
private GameManagerService mService;
+ @Nullable
+ private GameServiceController mGameServiceController;
public Lifecycle(Context context) {
super(context);
@@ -505,32 +507,49 @@ public final class GameManagerService extends IGameManagerService.Stub {
@Override
public void onStart() {
- mService = new GameManagerService(getContext());
+ final Context context = getContext();
+ mService = new GameManagerService(context);
publishBinderService(Context.GAME_SERVICE, mService);
mService.registerDeviceConfigListener();
mService.registerPackageReceiver();
+
+ if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_GAME_SERVICE)) {
+ mGameServiceController = new GameServiceController(context);
+ }
}
@Override
public void onBootPhase(int phase) {
if (phase == PHASE_BOOT_COMPLETED) {
mService.onBootCompleted();
+ if (mGameServiceController != null) {
+ mGameServiceController.onBootComplete();
+ }
}
}
@Override
public void onUserStarting(@NonNull TargetUser user) {
mService.onUserStarting(user.getUserIdentifier());
+ if (mGameServiceController != null) {
+ mGameServiceController.notifyUserStarted(user);
+ }
}
@Override
public void onUserStopping(@NonNull TargetUser user) {
mService.onUserStopping(user.getUserIdentifier());
+ if (mGameServiceController != null) {
+ mGameServiceController.notifyUserStopped(user);
+ }
}
@Override
public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) {
mService.onUserSwitching(from, to.getUserIdentifier());
+ if (mGameServiceController != null) {
+ mGameServiceController.notifyNewForegroundUser(to);
+ }
}
}
diff --git a/services/core/java/com/android/server/app/GameServiceConnection.java b/services/core/java/com/android/server/app/GameServiceConnection.java
new file mode 100644
index 0000000000000..b60778915de78
--- /dev/null
+++ b/services/core/java/com/android/server/app/GameServiceConnection.java
@@ -0,0 +1,102 @@
+/*
+ * 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 com.android.server.app;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.service.games.GameService;
+import android.service.games.IGameService;
+import android.util.Slog;
+
+final class GameServiceConnection {
+ private static final String TAG = "GameServiceConnection";
+ private static final boolean DEBUG = false;
+
+ private final Context mContext;
+ private final ComponentName mGameServiceComponent;
+ private final int mUser;
+ private boolean mIsBound;
+ @Nullable
+ private IGameService mGameService;
+ private final ServiceConnection mConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ if (DEBUG) {
+ Slog.d(TAG, "onServiceConnected to " + name + " for user(" + mUser + ")");
+ }
+
+ mGameService = IGameService.Stub.asInterface(service);
+ try {
+ mGameService.connected();
+ } catch (RemoteException e) {
+ Slog.w(TAG, "RemoteException while calling ready", e);
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (DEBUG) {
+ Slog.d(TAG, "onServiceDisconnected to " + name);
+ }
+
+ mGameService = null;
+ }
+ };
+
+ GameServiceConnection(Context context, ComponentName gameServiceComponent, int user) {
+ mContext = context;
+ mGameServiceComponent = gameServiceComponent;
+ mUser = user;
+ }
+
+ public void connect() {
+ if (mIsBound) {
+ Slog.v(TAG, "Already bound, ignoring start.");
+ return;
+ }
+
+ Intent intent = new Intent(GameService.SERVICE_INTERFACE);
+ intent.setComponent(mGameServiceComponent);
+ mIsBound = mContext.bindServiceAsUser(intent, mConnection,
+ Context.BIND_AUTO_CREATE
+ | Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS, new UserHandle(mUser));
+ if (!mIsBound) {
+ Slog.w(TAG, "Failed binding to game service " + mGameServiceComponent);
+ }
+ }
+
+ public void disconnect() {
+ try {
+ if (mGameService != null) {
+ mGameService.disconnected();
+ }
+ } catch (RemoteException e) {
+ Slog.w(TAG, "RemoteException in shutdown", e);
+ }
+
+ if (mIsBound) {
+ mContext.unbindService(mConnection);
+ mIsBound = false;
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/app/GameServiceController.java b/services/core/java/com/android/server/app/GameServiceController.java
new file mode 100644
index 0000000000000..d056ea9f359a8
--- /dev/null
+++ b/services/core/java/com/android/server/app/GameServiceController.java
@@ -0,0 +1,154 @@
+/*
+ * 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 com.android.server.app;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.service.games.GameService;
+import android.text.TextUtils;
+import android.util.Slog;
+
+import com.android.server.SystemService;
+
+import java.util.List;
+
+final class GameServiceController {
+ private static final String TAG = "GameServiceController";
+ private static final boolean DEBUG = false;
+
+ private final Context mContext;
+ @Nullable
+ private SystemService.TargetUser mCurrentForegroundUser;
+ private boolean mHasBootCompleted;
+
+ @Nullable
+ private GameServiceConnection mGameServiceConnection;
+
+ GameServiceController(Context context) {
+ mContext = context;
+ }
+
+ void onBootComplete() {
+ mHasBootCompleted = true;
+
+ evaluateGameServiceConnection();
+ }
+
+ void notifyUserStarted(@NonNull SystemService.TargetUser user) {
+ if (mCurrentForegroundUser != null) {
+ return;
+ }
+
+ mCurrentForegroundUser = user;
+ evaluateGameServiceConnection();
+ }
+
+ void notifyNewForegroundUser(@NonNull SystemService.TargetUser user) {
+ mCurrentForegroundUser = user;
+ evaluateGameServiceConnection();
+ }
+
+ void notifyUserStopped(@NonNull SystemService.TargetUser user) {
+ if (mCurrentForegroundUser == null
+ || mCurrentForegroundUser.getUserIdentifier() != user.getUserIdentifier()) {
+ return;
+ }
+
+ mCurrentForegroundUser = null;
+ evaluateGameServiceConnection();
+ }
+
+ private void evaluateGameServiceConnection() {
+ if (!mHasBootCompleted) {
+ return;
+ }
+
+ // TODO(b/204565942): Only shutdown the existing service connection if the game service
+ // provider or user has changed.
+ if (mGameServiceConnection != null) {
+ mGameServiceConnection.disconnect();
+ mGameServiceConnection = null;
+ }
+
+ boolean isUserSupported =
+ mCurrentForegroundUser != null
+ && mCurrentForegroundUser.isFull()
+ && !mCurrentForegroundUser.isManagedProfile();
+ if (!isUserSupported) {
+ if (DEBUG && mCurrentForegroundUser != null) {
+ Slog.d(TAG, "User not supported: " + mCurrentForegroundUser);
+ }
+ return;
+ }
+
+ ComponentName gameServiceComponentName =
+ determineGameServiceComponentName(mCurrentForegroundUser.getUserIdentifier());
+ if (gameServiceComponentName == null) {
+ return;
+ }
+
+ mGameServiceConnection = new GameServiceConnection(
+ mContext,
+ gameServiceComponentName,
+ mCurrentForegroundUser.getUserIdentifier());
+ mGameServiceConnection.connect();
+ }
+
+ @Nullable
+ private ComponentName determineGameServiceComponentName(int userId) {
+ String gameServicePackage =
+ mContext.getResources().getString(
+ com.android.internal.R.string.config_systemGameService);
+ if (TextUtils.isEmpty(gameServicePackage)) {
+ if (DEBUG) {
+ Slog.d(TAG, "No game service package defined");
+ }
+ return null;
+ }
+
+ List gameServiceResolveInfos =
+ mContext.getPackageManager().queryIntentServicesAsUser(
+ new Intent(GameService.SERVICE_INTERFACE).setPackage(gameServicePackage),
+ PackageManager.MATCH_SYSTEM_ONLY,
+ userId);
+
+ if (gameServiceResolveInfos.isEmpty()) {
+ Slog.v(TAG, "No available game service found for user id: " + userId);
+ return null;
+ }
+
+ for (ResolveInfo resolveInfo : gameServiceResolveInfos) {
+ if (resolveInfo.serviceInfo == null) {
+ continue;
+ }
+ final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+ if (!serviceInfo.isEnabled()) {
+ continue;
+ }
+ return serviceInfo.getComponentName();
+ }
+
+ Slog.v(TAG, "No game service found for user id: " + userId);
+ return null;
+ }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/app/GameServiceControllerTests.java b/services/tests/mockingservicestests/src/com/android/server/app/GameServiceControllerTests.java
new file mode 100644
index 0000000000000..8973a897e8889
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/app/GameServiceControllerTests.java
@@ -0,0 +1,455 @@
+/*
+ * 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 com.android.server.app;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.pm.UserInfo;
+import android.content.res.Resources;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.platform.test.annotations.Presubmit;
+import android.service.games.GameService;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.SystemService;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoSession;
+
+import java.util.List;
+
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public final class GameServiceControllerTests {
+ @Mock
+ private PackageManager mMockPackageManager;
+ @Mock
+ private Resources mMockResources;
+ @Mock
+ private Context mMockContext;
+ private MockitoSession mMockingSession;
+
+ private static UserInfo eligibleUserInfo(int uid) {
+ return new UserInfo(uid, "", "", UserInfo.FLAG_FULL);
+ }
+
+ private static UserInfo managedUserInfo(int uid) {
+ UserInfo userInfo = eligibleUserInfo(uid);
+ userInfo.userType = UserManager.USER_TYPE_PROFILE_MANAGED;
+ return userInfo;
+ }
+
+ private static ResolveInfo resolveInfo(ServiceInfo serviceInfo) {
+ ResolveInfo resolveInfo = new ResolveInfo();
+ resolveInfo.serviceInfo = serviceInfo;
+ return resolveInfo;
+ }
+
+ private static ServiceInfo serviceInfo(String packageName, String name, boolean isEnabled) {
+ ApplicationInfo applicationInfo = new ApplicationInfo();
+ applicationInfo.packageName = packageName;
+ applicationInfo.enabled = true;
+
+ ServiceInfo serviceInfo = new ServiceInfo();
+ serviceInfo.applicationInfo = applicationInfo;
+ serviceInfo.packageName = packageName;
+ serviceInfo.name = name;
+ serviceInfo.enabled = isEnabled;
+ return serviceInfo;
+ }
+
+ private static SystemService.TargetUser managedTargetUser(int ineligibleUserId) {
+ return new SystemService.TargetUser(managedUserInfo(ineligibleUserId));
+ }
+
+ private static SystemService.TargetUser eligibleTargetUser(int userId) {
+ return new SystemService.TargetUser(eligibleUserInfo(userId));
+ }
+
+ private static UserHandle userWithId(int userId) {
+ return argThat(userInfo -> userInfo.getIdentifier() == userId);
+ }
+
+ @Before
+ public void setUp() {
+ mMockingSession = mockitoSession()
+ .initMocks(this)
+ .startMocking();
+ }
+
+ @After
+ public void tearDown() {
+ mMockingSession.finishMocking();
+ }
+
+ @Test
+ public void testStartConnectionOnBootWithNoUser() {
+ GameServiceController gameServiceController =
+ new GameServiceController(mMockContext);
+
+ gameServiceController.onBootComplete();
+
+ verifyNoServiceBound();
+ }
+
+ @Test
+ public void testStartConnectionOnBootWithManagedUser() {
+ int userId = 12345;
+ GameServiceController gameServiceController =
+ new GameServiceController(mMockContext);
+
+ gameServiceController.notifyUserStarted(managedTargetUser(userId));
+ gameServiceController.onBootComplete();
+
+ verifyNoServiceBound();
+ }
+
+ @Test
+ public void testStartConnectionOnBootWithUserAndNoSystemGamesServiceSet() {
+ seedSystemGameServicePackageName("");
+
+ GameServiceController gameServiceController =
+ new GameServiceController(mMockContext);
+
+ gameServiceController.notifyUserStarted(eligibleTargetUser(1000));
+ gameServiceController.onBootComplete();
+
+ verifyNoServiceBound();
+ }
+
+ @Test
+ public void testStartConnectionOnBootWithUserAndSystemGamesServiceDoesNotExist() {
+ int userId = 12345;
+ String gameServicePackageName = "game.service.package";
+ seedSystemGameServicePackageName(gameServicePackageName);
+ seedGameServiceResolveInfos(gameServicePackageName, userId, ImmutableList.of());
+
+ GameServiceController gameServiceController =
+ new GameServiceController(mMockContext);
+
+ gameServiceController.notifyUserStarted(eligibleTargetUser(userId));
+ gameServiceController.onBootComplete();
+
+ verifyNoServiceBound();
+ }
+
+ @Test
+ public void testStartConnectionOnBootWithUserAndSystemGamesServiceSet() {
+ int userId = 12345;
+ String gameServicePackageName = "game.service.package";
+ String gameServiceComponent = "game.service.package.example.GameService";
+ seedSystemGameServicePackageName(gameServicePackageName);
+ seedGameServiceResolveInfos(gameServicePackageName, userId, ImmutableList.of(
+ resolveInfo(serviceInfo(gameServicePackageName, gameServiceComponent, true))));
+
+ GameServiceController gameServiceController =
+ new GameServiceController(mMockContext);
+
+ gameServiceController.notifyUserStarted(eligibleTargetUser(userId));
+ gameServiceController.onBootComplete();
+
+ verifyServiceBoundForUserAndComponent(userId, gameServicePackageName, gameServiceComponent);
+ }
+
+ @Test
+ public void testStartConnectionOnBootWithUserAndSystemGamesServiceNotEnabled() {
+ int userId = 12345;
+ String gameServicePackageName = "game.service.package";
+ String gameServiceComponent = "game.service.package.example.GameService";
+ seedSystemGameServicePackageName(gameServicePackageName);
+ seedGameServiceResolveInfos(gameServicePackageName, userId, ImmutableList.of(
+ resolveInfo(serviceInfo(gameServicePackageName, gameServiceComponent, false))));
+
+ GameServiceController gameServiceController =
+ new GameServiceController(mMockContext);
+
+ gameServiceController.notifyUserStarted(eligibleTargetUser(userId));
+ gameServiceController.onBootComplete();
+
+ verifyNoServiceBound();
+ }
+
+ @Test
+ public void testStartConnectionOnBootWithUserAndSystemGamesServiceHasMultipleComponents() {
+ int userId = 12345;
+ String gameServicePackageName = "game.service.package";
+ String gameServiceComponent1 = "game.service.package.example.GameService1";
+ String gameServiceComponent2 = "game.service.package.example.GameService2";
+ seedSystemGameServicePackageName(gameServicePackageName);
+ seedGameServiceResolveInfos(gameServicePackageName, userId, ImmutableList.of(
+ resolveInfo(serviceInfo(gameServicePackageName, gameServiceComponent1, true)),
+ resolveInfo(serviceInfo(gameServicePackageName, gameServiceComponent2, true))));
+
+ GameServiceController gameServiceController =
+ new GameServiceController(mMockContext);
+
+ gameServiceController.notifyUserStarted(eligibleTargetUser(userId));
+ gameServiceController.onBootComplete();
+
+ verifyServiceBoundForUserAndComponent(userId, gameServicePackageName,
+ gameServiceComponent1);
+ }
+
+ @Test
+ public void testStartConnectionOnBootWithUserAndSystemGamesServiceHasDisabledComponent() {
+ int userId = 12345;
+ String gameServicePackageName = "game.service.package";
+ String gameServiceComponent1 = "game.service.package.example.GameService1";
+ String gameServiceComponent2 = "game.service.package.example.GameService2";
+ seedSystemGameServicePackageName(gameServicePackageName);
+ seedGameServiceResolveInfos(gameServicePackageName, userId, ImmutableList.of(
+ resolveInfo(serviceInfo(gameServicePackageName, gameServiceComponent1, false)),
+ resolveInfo(serviceInfo(gameServicePackageName, gameServiceComponent2, true))));
+
+ GameServiceController gameServiceController =
+ new GameServiceController(mMockContext);
+
+ gameServiceController.notifyUserStarted(eligibleTargetUser(userId));
+ gameServiceController.onBootComplete();
+
+ verifyServiceBoundForUserAndComponent(userId, gameServicePackageName,
+ gameServiceComponent2);
+ }
+
+ @Test
+ public void testSwitchFromEligibleUserToEligibleUser() {
+ int userId1 = 1;
+ int userId2 = 2;
+ String gameServicePackageName = "game.service.package";
+ String gameServiceComponent = "game.service.package.example.GameService";
+ seedSystemGameServicePackageName(gameServicePackageName);
+ seedGameServiceResolveInfos(gameServicePackageName, userId1, ImmutableList.of(
+ resolveInfo(serviceInfo(gameServicePackageName, gameServiceComponent, true))));
+ seedGameServiceResolveInfos(gameServicePackageName, userId2, ImmutableList.of(
+ resolveInfo(serviceInfo(gameServicePackageName, gameServiceComponent, true))));
+ seedGameServiceToBindSuccessfully();
+
+ GameServiceController gameServiceController = new GameServiceController(mMockContext);
+
+ gameServiceController.onBootComplete();
+ gameServiceController.notifyUserStarted(eligibleTargetUser(userId1));
+
+ verifyServiceBoundForUserAndComponent(userId1, gameServicePackageName,
+ gameServiceComponent);
+
+ gameServiceController.notifyNewForegroundUser(eligibleTargetUser(userId2));
+
+ verify(mMockContext).unbindService(any());
+ verifyServiceBoundForUserAndComponent(userId2, gameServicePackageName,
+ gameServiceComponent);
+ }
+
+ @Test
+ public void testSwitchFromEligibleUserToIneligibleUser() {
+ int eligibleUserId = 1;
+ int ineligibleUserId = 2;
+ String gameServicePackageName = "game.service.package";
+ String gameServiceComponent = "game.service.package.example.GameService";
+ seedSystemGameServicePackageName(gameServicePackageName);
+ seedGameServiceResolveInfos(gameServicePackageName, eligibleUserId, ImmutableList.of(
+ resolveInfo(serviceInfo(gameServicePackageName, gameServiceComponent, true))));
+ seedGameServiceToBindSuccessfully();
+
+ GameServiceController gameServiceController =
+ new GameServiceController(mMockContext);
+
+ gameServiceController.onBootComplete();
+ gameServiceController.notifyUserStarted(eligibleTargetUser(eligibleUserId));
+
+ verifyServiceBoundForUserAndComponent(eligibleUserId, gameServicePackageName,
+ gameServiceComponent);
+
+ gameServiceController.notifyNewForegroundUser(managedTargetUser(ineligibleUserId));
+
+ verify(mMockContext).unbindService(any());
+ }
+
+ @Test
+ public void testSwitchFromIneligibleUserToEligibleUser() {
+ int eligibleUserId = 1;
+ int ineligibleUserId = 2;
+ String gameServicePackageName = "game.service.package";
+ String gameServiceComponent = "game.service.package.example.GameService";
+ seedSystemGameServicePackageName(gameServicePackageName);
+ seedGameServiceResolveInfos(gameServicePackageName, eligibleUserId, ImmutableList.of(
+ resolveInfo(serviceInfo(gameServicePackageName, gameServiceComponent, true))));
+ seedGameServiceToBindSuccessfully();
+
+ GameServiceController gameServiceController = new GameServiceController(mMockContext);
+
+ gameServiceController.onBootComplete();
+ gameServiceController.notifyUserStarted(managedTargetUser(ineligibleUserId));
+
+ verifyNoServiceBound();
+
+ gameServiceController.notifyNewForegroundUser(eligibleTargetUser(eligibleUserId));
+
+ verifyServiceBoundForUserAndComponent(eligibleUserId, gameServicePackageName,
+ gameServiceComponent);
+ }
+
+ @Test
+ public void testMultipleRunningUsers() {
+ int userId1 = 123;
+ int userId2 = 456;
+ String gameServicePackageName = "game.service.package";
+ String gameServiceComponent = "game.service.package.example.GameService";
+ seedSystemGameServicePackageName(gameServicePackageName);
+ seedGameServiceResolveInfos(gameServicePackageName, userId1, ImmutableList.of(
+ resolveInfo(serviceInfo(gameServicePackageName, gameServiceComponent, true))));
+ seedGameServiceToBindSuccessfully();
+
+ GameServiceController gameServiceController =
+ new GameServiceController(mMockContext);
+
+ gameServiceController.onBootComplete();
+ gameServiceController.notifyUserStarted(eligibleTargetUser(userId1));
+ gameServiceController.notifyUserStarted(eligibleTargetUser(userId2));
+
+ verifyServiceBoundForUserAndComponent(userId1, gameServicePackageName,
+ gameServiceComponent);
+ verifyServiceNotBoundForUser(userId2);
+ verify(mMockContext, never()).unbindService(any());
+ }
+
+ @Test
+ public void testForegroundUserStopped() {
+ int userId = 123123;
+ String gameServicePackageName = "game.service.package";
+ String gameServiceComponent = "game.service.package.example.GameService";
+ seedSystemGameServicePackageName(gameServicePackageName);
+ seedGameServiceResolveInfos(gameServicePackageName, userId, ImmutableList.of(
+ resolveInfo(serviceInfo(gameServicePackageName, gameServiceComponent, true))));
+ seedGameServiceToBindSuccessfully();
+
+ GameServiceController gameServiceController =
+ new GameServiceController(mMockContext);
+
+ gameServiceController.onBootComplete();
+ gameServiceController.notifyUserStarted(eligibleTargetUser(userId));
+
+ verifyServiceBoundForUserAndComponent(userId, gameServicePackageName, gameServiceComponent);
+
+ gameServiceController.notifyUserStopped(eligibleTargetUser(userId));
+
+ verify(mMockContext).unbindService(any());
+ }
+
+ @Test
+ public void testNonForegroundUserStopped() {
+ int userId1 = 123;
+ int userId2 = 456;
+ String gameServicePackageName = "game.service.package";
+ String gameServiceComponent = "game.service.package.example.GameService";
+ seedSystemGameServicePackageName(gameServicePackageName);
+ seedGameServiceResolveInfos(gameServicePackageName, userId1, ImmutableList.of(
+ resolveInfo(serviceInfo(gameServicePackageName, gameServiceComponent, true))));
+ seedGameServiceResolveInfos(gameServicePackageName, userId2, ImmutableList.of(
+ resolveInfo(serviceInfo(gameServicePackageName, gameServiceComponent, true))));
+ seedGameServiceToBindSuccessfully();
+
+ GameServiceController gameServiceController =
+ new GameServiceController(mMockContext);
+ InOrder inOrder = Mockito.inOrder(mMockContext);
+
+ gameServiceController.onBootComplete();
+ gameServiceController.notifyUserStarted(eligibleTargetUser(userId1));
+
+ inOrder.verify(mMockContext).bindServiceAsUser(any(), any(), anyInt(), userWithId(userId1));
+
+ gameServiceController.notifyNewForegroundUser(eligibleTargetUser(userId2));
+
+ inOrder.verify(mMockContext).unbindService(any());
+ inOrder.verify(mMockContext).bindServiceAsUser(any(), any(), anyInt(), userWithId(userId2));
+
+ gameServiceController.notifyUserStopped(eligibleTargetUser(userId1));
+
+ inOrder.verify(mMockContext, never()).unbindService(any());
+ }
+
+ private void seedSystemGameServicePackageName(String gameServicePackageName) {
+ when(mMockContext.getResources()).thenReturn(mMockResources);
+ when(mMockResources.getString(com.android.internal.R.string.config_systemGameService))
+ .thenReturn(gameServicePackageName);
+ }
+
+ private void seedGameServiceResolveInfos(String gameServicePackageName, int userId,
+ List resolveInfos) {
+ when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+ doReturn(resolveInfos)
+ .when(mMockPackageManager).queryIntentServicesAsUser(
+ argThat(intent ->
+ intent != null
+ && intent.getAction().equals(GameService.SERVICE_INTERFACE)
+ && intent.getPackage().equals(gameServicePackageName)
+ ),
+ eq(PackageManager.MATCH_SYSTEM_ONLY),
+ eq(userId));
+ }
+
+ private void seedGameServiceToBindSuccessfully() {
+ when(mMockContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true);
+ }
+
+ private void verifyNoServiceBound() {
+ verify(mMockContext, never()).bindServiceAsUser(any(), any(), anyInt(), any());
+ }
+
+ private void verifyServiceBoundForUserAndComponent(int userId, String gameServicePackageName,
+ String gameServiceComponent) {
+ verify(mMockContext).bindServiceAsUser(
+ argThat(intent -> intent.getAction().equals(GameService.SERVICE_INTERFACE)
+ && intent.getComponent().getPackageName().equals(gameServicePackageName)
+ && intent.getComponent().getClassName().equals(gameServiceComponent)),
+ any(),
+ anyInt(), argThat(userInfo -> userInfo.getIdentifier() == userId));
+ }
+
+ private void verifyServiceNotBoundForUser(int userId) {
+ verify(mMockContext, never()).bindServiceAsUser(
+ any(),
+ any(),
+ anyInt(), userWithId(userId));
+ }
+}