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:
committed by
Shannon Chen
parent
8eb8f98e74
commit
13bfe79683
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
118
core/java/android/service/games/GameService.java
Normal file
118
core/java/android/service/games/GameService.java
Normal 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() {}
|
||||
}
|
||||
25
core/java/android/service/games/IGameService.aidl
Normal file
25
core/java/android/service/games/IGameService.aidl
Normal 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();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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. -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user