diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java index db44568eaf5f9..1302e226eba31 100644 --- a/services/core/java/com/android/server/app/GameManagerService.java +++ b/services/core/java/com/android/server/app/GameManagerService.java @@ -99,6 +99,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; +import java.util.Map; /** * Service to manage game related features. @@ -119,7 +120,7 @@ public final class GameManagerService extends IGameManagerService.Stub { static final int SET_GAME_STATE = 4; static final int CANCEL_GAME_LOADING_MODE = 5; static final int WRITE_GAME_MODE_INTERVENTION_LIST_FILE = 6; - static final int WRITE_SETTINGS_DELAY = 10 * 1000; // 10 seconds + static final int WRITE_DELAY_MILLIS = 10 * 1000; // 10 seconds static final int LOADING_BOOST_MAX_DURATION = 5 * 1000; // 5 seconds private static final String PACKAGE_NAME_MSG_KEY = "packageName"; @@ -130,7 +131,8 @@ public final class GameManagerService extends IGameManagerService.Stub { private final Context mContext; private final Object mLock = new Object(); private final Object mDeviceConfigLock = new Object(); - private final Handler mHandler; + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + final Handler mHandler; private final PackageManager mPackageManager; private final UserManager mUserManager; private final PowerManagerInternal mPowerManagerInternal; @@ -255,13 +257,13 @@ public final class GameManagerService extends IGameManagerService.Stub { if (userId < 0) { Slog.wtf(TAG, "Attempt to write settings for invalid user: " + userId); synchronized (mLock) { - removeMessages(WRITE_SETTINGS, msg.obj); + removeEqualMessages(WRITE_SETTINGS, msg.obj); } break; } Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT); synchronized (mLock) { - removeMessages(WRITE_SETTINGS, msg.obj); + removeEqualMessages(WRITE_SETTINGS, msg.obj); if (mSettings.containsKey(userId)) { GameManagerSettings userSettings = mSettings.get(userId); userSettings.writePersistentDataLocked(); @@ -275,8 +277,8 @@ public final class GameManagerService extends IGameManagerService.Stub { if (userId < 0) { Slog.wtf(TAG, "Attempt to write settings for invalid user: " + userId); synchronized (mLock) { - removeMessages(WRITE_SETTINGS, msg.obj); - removeMessages(REMOVE_SETTINGS, msg.obj); + removeEqualMessages(WRITE_SETTINGS, msg.obj); + removeEqualMessages(REMOVE_SETTINGS, msg.obj); } break; } @@ -284,8 +286,8 @@ public final class GameManagerService extends IGameManagerService.Stub { synchronized (mLock) { // Since the user was removed, ignore previous write message // and do write here. - removeMessages(WRITE_SETTINGS, msg.obj); - removeMessages(REMOVE_SETTINGS, msg.obj); + removeEqualMessages(WRITE_SETTINGS, msg.obj); + removeEqualMessages(REMOVE_SETTINGS, msg.obj); if (mSettings.containsKey(userId)) { final GameManagerSettings userSettings = mSettings.get(userId); mSettings.remove(userId); @@ -295,7 +297,7 @@ public final class GameManagerService extends IGameManagerService.Stub { break; } case POPULATE_GAME_MODE_SETTINGS: { - removeMessages(POPULATE_GAME_MODE_SETTINGS, msg.obj); + removeEqualMessages(POPULATE_GAME_MODE_SETTINGS, msg.obj); final int userId = (int) msg.obj; final String[] packageNames = getInstalledGamePackageNames(userId); updateConfigsForUser(userId, false /*checkGamePackage*/, packageNames); @@ -341,13 +343,13 @@ public final class GameManagerService extends IGameManagerService.Stub { if (userId < 0) { Slog.wtf(TAG, "Attempt to write setting for invalid user: " + userId); synchronized (mLock) { - removeMessages(WRITE_GAME_MODE_INTERVENTION_LIST_FILE, null); + removeEqualMessages(WRITE_GAME_MODE_INTERVENTION_LIST_FILE, msg.obj); } break; } Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT); - removeMessages(WRITE_GAME_MODE_INTERVENTION_LIST_FILE, null); + removeEqualMessages(WRITE_GAME_MODE_INTERVENTION_LIST_FILE, msg.obj); writeGameModeInterventionsToFile(userId); Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); break; @@ -843,7 +845,8 @@ public final class GameManagerService extends IGameManagerService.Stub { @Override public void onUserStarting(@NonNull TargetUser user) { - mService.onUserStarting(user); + mService.onUserStarting(user, + Environment.getDataSystemDeDirectory(user.getUserIdentifier())); } @Override @@ -1010,18 +1013,11 @@ public final class GameManagerService extends IGameManagerService.Stub { } GameManagerSettings userSettings = mSettings.get(userId); userSettings.setGameModeLocked(packageName, gameMode); - final Message msg = mHandler.obtainMessage(WRITE_SETTINGS); - msg.obj = userId; - if (!mHandler.hasEqualMessages(WRITE_SETTINGS, userId)) { - mHandler.sendMessageDelayed(msg, WRITE_SETTINGS_DELAY); - } } updateInterventions(packageName, gameMode, userId); - final Message msg = mHandler.obtainMessage(WRITE_GAME_MODE_INTERVENTION_LIST_FILE); - msg.obj = userId; - if (!mHandler.hasEqualMessages(WRITE_GAME_MODE_INTERVENTION_LIST_FILE, userId)) { - mHandler.sendMessage(msg); - } + sendUserMessage(userId, WRITE_SETTINGS, "SET_GAME_MODE", WRITE_DELAY_MILLIS); + sendUserMessage(userId, WRITE_GAME_MODE_INTERVENTION_LIST_FILE, + "SET_GAME_MODE", 0 /*delayMillis*/); } /** @@ -1208,22 +1204,46 @@ public final class GameManagerService extends IGameManagerService.Stub { if (mGameServiceController != null) { mGameServiceController.onBootComplete(); } + mContext.registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (Intent.ACTION_SHUTDOWN.equals(intent.getAction())) { + synchronized (mLock) { + // Note that the max wait time of broadcast is 10s (see + // {@ShutdownThread#MAX_BROADCAST_TIMEMAX_BROADCAST_TIME}) currently so + // this can be optional only if we have message delay plus processing + // time significant smaller to prevent data loss. + for (Map.Entry entry : mSettings.entrySet()) { + final int userId = entry.getKey(); + sendUserMessage(userId, WRITE_SETTINGS, + Intent.ACTION_SHUTDOWN, 0 /*delayMillis*/); + sendUserMessage(userId, + WRITE_GAME_MODE_INTERVENTION_LIST_FILE, Intent.ACTION_SHUTDOWN, + 0 /*delayMillis*/); + } + } + } + } + }, new IntentFilter(Intent.ACTION_SHUTDOWN)); } - void onUserStarting(@NonNull TargetUser user) { - final int userId = user.getUserIdentifier(); + private void sendUserMessage(int userId, int what, String eventForLog, int delayMillis) { + Message msg = mHandler.obtainMessage(what, userId); + if (!mHandler.sendMessageDelayed(msg, delayMillis)) { + Slog.e(TAG, "Failed to send user message " + what + " on " + eventForLog); + } + } + void onUserStarting(@NonNull TargetUser user, File settingDataDir) { + final int userId = user.getUserIdentifier(); synchronized (mLock) { if (!mSettings.containsKey(userId)) { - GameManagerSettings userSettings = - new GameManagerSettings(Environment.getDataSystemDeDirectory(userId)); + GameManagerSettings userSettings = new GameManagerSettings(settingDataDir); mSettings.put(userId, userSettings); userSettings.readPersistentDataLocked(); } } - final Message msg = mHandler.obtainMessage(POPULATE_GAME_MODE_SETTINGS); - msg.obj = userId; - mHandler.sendMessage(msg); + sendUserMessage(userId, POPULATE_GAME_MODE_SETTINGS, "ON_USER_STARTING", 0 /*delayMillis*/); if (mGameServiceController != null) { mGameServiceController.notifyUserStarted(user); @@ -1243,9 +1263,7 @@ public final class GameManagerService extends IGameManagerService.Stub { if (!mSettings.containsKey(userId)) { return; } - final Message msg = mHandler.obtainMessage(REMOVE_SETTINGS); - msg.obj = userId; - mHandler.sendMessage(msg); + sendUserMessage(userId, REMOVE_SETTINGS, "ON_USER_STOPPING", 0 /*delayMillis*/); } if (mGameServiceController != null) { @@ -1259,15 +1277,14 @@ public final class GameManagerService extends IGameManagerService.Stub { synchronized (mLock) { final int fromUserId = from.getUserIdentifier(); if (mSettings.containsKey(fromUserId)) { - final Message msg = mHandler.obtainMessage(REMOVE_SETTINGS); - msg.obj = fromUserId; - mHandler.sendMessage(msg); + sendUserMessage(fromUserId, REMOVE_SETTINGS, "ON_USER_SWITCHING", + 0 /*delayMillis*/); } } } - final Message msg = mHandler.obtainMessage(POPULATE_GAME_MODE_SETTINGS); - msg.obj = toUserId; - mHandler.sendMessage(msg); + + sendUserMessage(toUserId, POPULATE_GAME_MODE_SETTINGS, "ON_USER_SWITCHING", + 0 /*delayMillis*/); if (mGameServiceController != null) { mGameServiceController.notifyNewForegroundUser(to); @@ -1539,15 +1556,11 @@ public final class GameManagerService extends IGameManagerService.Stub { updateInterventions(packageName, gameMode, userId); } } + sendUserMessage(userId, WRITE_GAME_MODE_INTERVENTION_LIST_FILE, + "UPDATE_CONFIGS_FOR_USERS", 0 /*delayMillis*/); } catch (Exception e) { Slog.e(TAG, "Failed to update configs for user " + userId + ": " + e); } - - final Message msg = mHandler.obtainMessage(WRITE_GAME_MODE_INTERVENTION_LIST_FILE); - msg.obj = userId; - if (!mHandler.hasEqualMessages(WRITE_GAME_MODE_INTERVENTION_LIST_FILE, userId)) { - mHandler.sendMessage(msg); - } } /* @@ -1700,18 +1713,11 @@ public final class GameManagerService extends IGameManagerService.Stub { if (mSettings.containsKey(userId)) { mSettings.get(userId).removeGame(packageName); } - Message msg = mHandler.obtainMessage(WRITE_SETTINGS); - msg.obj = userId; - if (!mHandler.hasEqualMessages(WRITE_SETTINGS, userId)) { - mHandler.sendMessageDelayed(msg, WRITE_SETTINGS_DELAY); - } - msg = mHandler.obtainMessage( - WRITE_GAME_MODE_INTERVENTION_LIST_FILE); - msg.obj = userId; - if (!mHandler.hasEqualMessages( - WRITE_GAME_MODE_INTERVENTION_LIST_FILE, userId)) { - mHandler.sendMessage(msg); - } + sendUserMessage(userId, WRITE_SETTINGS, + Intent.ACTION_PACKAGE_REMOVED, WRITE_DELAY_MILLIS); + sendUserMessage(userId, + WRITE_GAME_MODE_INTERVENTION_LIST_FILE, + Intent.ACTION_PACKAGE_REMOVED, WRITE_DELAY_MILLIS); } } break; 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 27fc0946b8270..9022db83d5ce8 100644 --- a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java @@ -17,8 +17,10 @@ package com.android.server.app; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.server.app.GameManagerService.WRITE_SETTINGS; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; @@ -35,11 +37,15 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.Manifest; +import android.annotation.Nullable; import android.app.GameManager; import android.app.GameModeInfo; import android.app.GameState; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.ContextWrapper; +import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -97,6 +103,7 @@ public class GameManagerServiceTests { private PowerManagerInternal mMockPowerManager; @Mock private UserManager mMockUserManager; + private BroadcastReceiver mShutDownActionReceiver; // Stolen from ConnectivityServiceTest.MockContext class MockContext extends ContextWrapper { @@ -165,6 +172,12 @@ public class GameManagerServiceTests { } throw new UnsupportedOperationException("Couldn't find system service: " + name); } + + @Override + public Intent registerReceiver(@Nullable BroadcastReceiver receiver, IntentFilter filter) { + mShutDownActionReceiver = receiver; + return null; + } } @Before @@ -200,15 +213,16 @@ public class GameManagerServiceTests { @After public void tearDown() throws Exception { LocalServices.removeServiceForTest(PowerManagerInternal.class); - GameManagerService gameManagerService = new GameManagerService(mMockContext); if (mMockingSession != null) { mMockingSession.finishMocking(); } + deleteFolder(InstrumentationRegistry.getTargetContext().getFilesDir()); } private void startUser(GameManagerService gameManagerService, int userId) { UserInfo userInfo = new UserInfo(userId, "name", 0); - gameManagerService.onUserStarting(new SystemService.TargetUser(userInfo)); + gameManagerService.onUserStarting(new SystemService.TargetUser(userInfo), + InstrumentationRegistry.getContext().getFilesDir()); mTestLooper.dispatchAll(); } @@ -1508,4 +1522,42 @@ public class GameManagerServiceTests { GameManager.GAME_MODE_BATTERY, USER_ID_1); }); } + + @Test + public void testWritingSettingFile_onShutdown() throws InterruptedException { + mockModifyGameModeGranted(); + mockDeviceConfigAll(); + GameManagerService gameManagerService = new GameManagerService(mMockContext); + gameManagerService.onBootCompleted(); + startUser(gameManagerService, USER_ID_1); + Thread.sleep(500); + gameManagerService.setGameModeConfigOverride("com.android.app1", USER_ID_1, + GameManager.GAME_MODE_BATTERY, "60", "0.5"); + gameManagerService.setGameMode("com.android.app1", USER_ID_1, + GameManager.GAME_MODE_PERFORMANCE); + GameManagerSettings settings = new GameManagerSettings( + InstrumentationRegistry.getContext().getFilesDir()); + Thread.sleep(500); + // no data written as delayed messages are queued + assertFalse(settings.readPersistentDataLocked()); + assertTrue(gameManagerService.mHandler.hasEqualMessages(WRITE_SETTINGS, USER_ID_1)); + Intent shutdown = new Intent(); + shutdown.setAction(Intent.ACTION_SHUTDOWN); + mShutDownActionReceiver.onReceive(mMockContext, shutdown); + Thread.sleep(500); + // data is written on processing new message with no delay on shutdown, + // and all queued messages should be removed + assertTrue(settings.readPersistentDataLocked()); + assertFalse(gameManagerService.mHandler.hasEqualMessages(WRITE_SETTINGS, USER_ID_1)); + } + + private static void deleteFolder(File folder) { + File[] files = folder.listFiles(); + if (files != null) { + for (File file : files) { + deleteFolder(file); + } + } + folder.delete(); + } }