Add the GameService skeleton and corresponding unit tests.

This change defines a basic GameService SPI and hooks into the GameManagerService in order to drive the start/stop APIs.

Test: atest FramworksMockingServicesTests:GameServiceManagerTests and
manaul e2e testing
Bug: 204504879
Bug: 202414447
Bug: 202417255
CTS-Coverage-Bug: 206128693
Change-Id: Ibda56cb0c023a307f83eae4091c5f63b02be339f
This commit is contained in:
shannonchen
2021-12-01 17:45:28 +00:00
committed by Shannon Chen
parent 8eb8f98e74
commit 13bfe79683
11 changed files with 912 additions and 1 deletions

View File

@@ -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 {

View File

@@ -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

View File

@@ -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() {}
}

View File

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

View File

@@ -3914,6 +3914,15 @@
<permission android:name="android.permission.BIND_WALLPAPER"
android:protectionLevel="signature|privileged" />
<!-- Must be required by a game service to ensure that only the
system can bind to it.
<p>Protection level: signature
@hide
-->
<permission android:name="android.permission.BIND_GAME_SERVICE"
android:protectionLevel="signature" />
<!-- Must be required by a {@link android.service.voice.VoiceInteractionService},
to ensure that only the system can bind to it.
<p>Protection level: signature

View File

@@ -2110,6 +2110,8 @@
<string name="config_systemWifiCoexManager" translatable="false"></string>
<!-- The name of the package that will hold the wellbeing role. -->
<string name="config_systemWellbeing" translatable="false"></string>
<!-- The name of the package that will hold the game service role. -->
<string name="config_systemGameService" translatable="false"></string>
<!-- The name of the package that will hold the television notification handler role -->
<string name="config_systemTelevisionNotificationHandler" translatable="false"></string>
<!-- The name of the package that will hold the system activity recognizer role. -->

View File

@@ -4619,4 +4619,6 @@
<java-symbol type="array" name="config_builtInDisplayIsRoundArray" />
<java-symbol type="integer" name="config_mashPressVibrateTimeOnPowerButton" />
<java-symbol type="string" name="config_systemGameService" />
</resources>

View File

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

View File

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

View File

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

View File

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