diff --git a/core/api/system-current.txt b/core/api/system-current.txt index fb8e0c7a57350..31d1d5d9c0256 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -2895,6 +2895,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"; @@ -10445,6 +10446,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 aada7eb2df8f4..af21c31040b83 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3914,6 +3914,15 @@ + + + + + + diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 0495122eaee79..fd91a97ad55ab 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4619,4 +4619,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)); + } +}