diff --git a/core/java/android/app/IGameManagerService.aidl b/core/java/android/app/IGameManagerService.aidl index 3d6ab6fd9478b..9a818e49ae4a2 100644 --- a/core/java/android/app/IGameManagerService.aidl +++ b/core/java/android/app/IGameManagerService.aidl @@ -20,6 +20,7 @@ import android.app.GameModeConfiguration; import android.app.GameModeInfo; import android.app.GameState; import android.app.IGameModeListener; +import android.app.IGameStateListener; /** * @hide @@ -49,4 +50,6 @@ interface IGameManagerService { void addGameModeListener(IGameModeListener gameModeListener); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_GAME_MODE)") void removeGameModeListener(IGameModeListener gameModeListener); + void addGameStateListener(IGameStateListener gameStateListener); + void removeGameStateListener(IGameStateListener gameStateListener); } diff --git a/core/java/android/app/IGameStateListener.aidl b/core/java/android/app/IGameStateListener.aidl new file mode 100644 index 0000000000000..34cff489d077d --- /dev/null +++ b/core/java/android/app/IGameStateListener.aidl @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 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.app; + +import android.app.GameState; + +/** @hide */ +interface IGameStateListener { + /** + * Called when the state of the game has changed. + */ + oneway void onGameStateChanged(String packageName, in GameState state, int userId); +} diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java index ca15dd79adbcb..c6d6122aeed6f 100644 --- a/services/core/java/com/android/server/app/GameManagerService.java +++ b/services/core/java/com/android/server/app/GameManagerService.java @@ -40,6 +40,7 @@ import android.app.GameModeInfo; import android.app.GameState; import android.app.IGameManagerService; import android.app.IGameModeListener; +import android.app.IGameStateListener; import android.app.StatsManager; import android.app.UidObserver; import android.content.BroadcastReceiver; @@ -148,6 +149,7 @@ public final class GameManagerService extends IGameManagerService.Stub { private final Object mLock = new Object(); private final Object mDeviceConfigLock = new Object(); private final Object mGameModeListenerLock = new Object(); + private final Object mGameStateListenerLock = new Object(); @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) final Handler mHandler; private final PackageManager mPackageManager; @@ -164,6 +166,8 @@ public final class GameManagerService extends IGameManagerService.Stub { // listener to caller uid map @GuardedBy("mGameModeListenerLock") private final ArrayMap mGameModeListeners = new ArrayMap<>(); + @GuardedBy("mGameStateListenerLock") + private final ArrayMap mGameStateListeners = new ArrayMap<>(); @Nullable private final GameServiceController mGameServiceController; private final Object mUidObserverLock = new Object(); @@ -352,6 +356,16 @@ public final class GameManagerService extends IGameManagerService.Stub { loadingBoostDuration); } } + synchronized (mGameStateListenerLock) { + for (IGameStateListener listener : mGameStateListeners.keySet()) { + try { + listener.onGameStateChanged(packageName, gameState, userId); + } catch (RemoteException ex) { + Slog.w(TAG, "Cannot notify game state change for listener added by " + + mGameStateListeners.get(listener)); + } + } + } break; } case CANCEL_GAME_LOADING_MODE: { @@ -1473,6 +1487,43 @@ public final class GameManagerService extends IGameManagerService.Stub { } } + /** + * Adds a game state listener. + */ + @Override + public void addGameStateListener(@NonNull IGameStateListener listener) { + try { + final IBinder listenerBinder = listener.asBinder(); + listenerBinder.linkToDeath(new DeathRecipient() { + @Override public void binderDied() { + removeGameStateListenerUnchecked(listener); + listenerBinder.unlinkToDeath(this, 0 /*flags*/); + } + }, 0 /*flags*/); + synchronized (mGameStateListenerLock) { + mGameStateListeners.put(listener, Binder.getCallingUid()); + } + } catch (RemoteException ex) { + Slog.e(TAG, + "Failed to link death recipient for IGameStateListener from caller " + + Binder.getCallingUid() + ", abandoned its listener registration", ex); + } + } + + /** + * Removes a game state listener. + */ + @Override + public void removeGameStateListener(@NonNull IGameStateListener listener) { + removeGameStateListenerUnchecked(listener); + } + + private void removeGameStateListenerUnchecked(IGameStateListener listener) { + synchronized (mGameStateListenerLock) { + mGameStateListeners.remove(listener); + } + } + /** * Notified when boot is completed. */ diff --git a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java index 212a243c6a9e0..cd3a78ed5e17d 100644 --- a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java @@ -52,6 +52,7 @@ import android.app.GameModeConfiguration; import android.app.GameModeInfo; import android.app.GameState; import android.app.IGameModeListener; +import android.app.IGameStateListener; import android.content.BroadcastReceiver; import android.content.Context; import android.content.ContextWrapper; @@ -1578,6 +1579,71 @@ public class GameManagerServiceTests { assertFalse(gameManagerService.mHandler.hasMessages(SET_GAME_STATE)); } + @Test + public void testAddGameStateListener() throws Exception { + mockModifyGameModeGranted(); + GameManagerService gameManagerService = + new GameManagerService(mMockContext, mTestLooper.getLooper()); + mockDeviceConfigAll(); + startUser(gameManagerService, USER_ID_1); + + IGameStateListener mockListener = Mockito.mock(IGameStateListener.class); + IBinder binder = Mockito.mock(IBinder.class); + when(mockListener.asBinder()).thenReturn(binder); + gameManagerService.addGameStateListener(mockListener); + verify(binder).linkToDeath(mDeathRecipientCaptor.capture(), anyInt()); + + mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_AUDIO); + GameState gameState = new GameState(true, GameState.MODE_NONE); + gameManagerService.setGameState(mPackageName, gameState, USER_ID_1); + assertFalse(gameManagerService.mHandler.hasMessages(SET_GAME_STATE)); + mTestLooper.dispatchAll(); + verify(mockListener, never()).onGameStateChanged(anyString(), any(), anyInt()); + + mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_GAME); + gameState = new GameState(true, GameState.MODE_NONE); + gameManagerService.setGameState(mPackageName, gameState, USER_ID_1); + assertTrue(gameManagerService.mHandler.hasMessages(SET_GAME_STATE)); + mTestLooper.dispatchAll(); + verify(mockListener).onGameStateChanged(mPackageName, gameState, USER_ID_1); + reset(mockListener); + + gameState = new GameState(false, GameState.MODE_CONTENT); + gameManagerService.setGameState(mPackageName, gameState, USER_ID_1); + mTestLooper.dispatchAll(); + verify(mockListener).onGameStateChanged(mPackageName, gameState, USER_ID_1); + reset(mockListener); + + mDeathRecipientCaptor.getValue().binderDied(); + verify(binder).unlinkToDeath(eq(mDeathRecipientCaptor.getValue()), anyInt()); + gameManagerService.setGameState(mPackageName, gameState, USER_ID_1); + assertTrue(gameManagerService.mHandler.hasMessages(SET_GAME_STATE)); + mTestLooper.dispatchAll(); + verify(mockListener, never()).onGameStateChanged(anyString(), any(), anyInt()); + } + + @Test + public void testRemoveGameStateListener() throws Exception { + mockModifyGameModeGranted(); + GameManagerService gameManagerService = + new GameManagerService(mMockContext, mTestLooper.getLooper()); + mockDeviceConfigAll(); + startUser(gameManagerService, USER_ID_1); + + IGameStateListener mockListener = Mockito.mock(IGameStateListener.class); + IBinder binder = Mockito.mock(IBinder.class); + when(mockListener.asBinder()).thenReturn(binder); + + gameManagerService.addGameStateListener(mockListener); + gameManagerService.removeGameStateListener(mockListener); + mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_GAME); + GameState gameState = new GameState(false, GameState.MODE_CONTENT); + gameManagerService.setGameState(mPackageName, gameState, USER_ID_1); + assertTrue(gameManagerService.mHandler.hasMessages(SET_GAME_STATE)); + mTestLooper.dispatchAll(); + verify(mockListener, never()).onGameStateChanged(anyString(), any(), anyInt()); + } + private List readGameModeInterventionList() throws Exception { final File interventionFile = new File(InstrumentationRegistry.getContext().getFilesDir(), "system/game_mode_intervention.list");