Improve handling of GameService when bound processes die.

- Add callbacks to observer service lifecycle events to ServiceConnector.
 - Leverage callbacks to avoid re-binding to GameService in order to
   send disconnected event
 - Leverage callbacks to detect when GameSessionService process dies and
   destroy all GameSessions.

Test: Manually verified GameSession overlays are removed when
GameSession process crashes. Added unit test covering behavior.
Bug: 220204229
Bug: 215599171

Change-Id: Idda96f221e40c88f0f80c95bea94556275da5166
This commit is contained in:
David Samuelson
2022-02-18 02:53:37 +00:00
parent 7a53fdf6f7
commit c9e4e76b2e
7 changed files with 455 additions and 24 deletions

View File

@@ -146,6 +146,15 @@ public interface ServiceConnector<I extends IInterface> {
*/
void unbind();
/**
* Registers a {@link ServiceLifecycleCallbacks callbacks} to be invoked when the lifecycle
* of the managed service changes.
*
* @param callbacks The callbacks that will be run, or {@code null} to clear the existing
* callbacks.
*/
void setServiceLifecycleCallbacks(@Nullable ServiceLifecycleCallbacks<I> callbacks);
/**
* A request to be run when the service is
* {@link ServiceConnection#onServiceConnected connected}.
@@ -189,6 +198,32 @@ public interface ServiceConnector<I extends IInterface> {
}
}
/**
* Collection of callbacks invoked when the lifecycle of the service changes.
*
* @param <II> the type of the {@link IInterface ipc interface} for the remote service
* @see ServiceConnector#setServiceLifecycleCallbacks(ServiceLifecycleCallbacks)
*/
interface ServiceLifecycleCallbacks<II extends IInterface> {
/**
* Called when the service has just connected and before any queued jobs are run.
*/
default void onConnected(@NonNull II service) {}
/**
* Called just before the service is disconnected and unbound.
*/
default void onDisconnected(@NonNull II service) {}
/**
* Called when the service Binder has died.
*
* In cases where {@link #onBinderDied()} is invoked the service becomes unbound without
* a callback to {@link #onDisconnected(IInterface)}.
*/
default void onBinderDied() {}
}
/**
* Implementation of {@link ServiceConnector}
@@ -230,6 +265,8 @@ public interface ServiceConnector<I extends IInterface> {
private final @NonNull Handler mHandler;
protected final @NonNull Executor mExecutor;
@Nullable
private volatile ServiceLifecycleCallbacks<I> mServiceLifecycleCallbacks = null;
private volatile I mService = null;
private boolean mBinding = false;
private boolean mUnbinding = false;
@@ -330,6 +367,19 @@ public interface ServiceConnector<I extends IInterface> {
}
}
private void dispatchOnServiceConnectionStatusChanged(
@NonNull I service, boolean isConnected) {
ServiceLifecycleCallbacks<I> serviceLifecycleCallbacks = mServiceLifecycleCallbacks;
if (serviceLifecycleCallbacks != null) {
if (isConnected) {
serviceLifecycleCallbacks.onConnected(service);
} else {
serviceLifecycleCallbacks.onDisconnected(service);
}
}
onServiceConnectionStatusChanged(service, isConnected);
}
/**
* Called when the service just connected or is about to disconnect
*/
@@ -504,12 +554,17 @@ public interface ServiceConnector<I extends IInterface> {
mHandler.post(this::unbindJobThread);
}
@Override
public void setServiceLifecycleCallbacks(@Nullable ServiceLifecycleCallbacks<I> callbacks) {
mServiceLifecycleCallbacks = callbacks;
}
void unbindJobThread() {
cancelTimeout();
I service = mService;
boolean wasBound = service != null;
if (wasBound) {
onServiceConnectionStatusChanged(service, false);
dispatchOnServiceConnectionStatusChanged(service, false);
mContext.unbindService(mServiceConnection);
service.asBinder().unlinkToDeath(this, 0);
mService = null;
@@ -560,7 +615,7 @@ public interface ServiceConnector<I extends IInterface> {
} catch (RemoteException e) {
Log.e(LOG_TAG, "onServiceConnected " + name + ": ", e);
}
onServiceConnectionStatusChanged(service, true);
dispatchOnServiceConnectionStatusChanged(service, true);
processQueue();
}
@@ -572,7 +627,7 @@ public interface ServiceConnector<I extends IInterface> {
mBinding = true;
I service = mService;
if (service != null) {
onServiceConnectionStatusChanged(service, false);
dispatchOnServiceConnectionStatusChanged(service, false);
mService = null;
}
}
@@ -592,6 +647,14 @@ public interface ServiceConnector<I extends IInterface> {
}
mService = null;
unbind();
dispatchOnBinderDied();
}
private void dispatchOnBinderDied() {
ServiceLifecycleCallbacks<I> serviceLifecycleCallbacks = mServiceLifecycleCallbacks;
if (serviceLifecycleCallbacks != null) {
serviceLifecycleCallbacks.onBinderDied();
}
}
@Override
@@ -764,5 +827,10 @@ public interface ServiceConnector<I extends IInterface> {
@Override
public void unbind() {}
@Override
public void setServiceLifecycleCallbacks(@Nullable ServiceLifecycleCallbacks<T> callbacks) {
// Do nothing.
}
}
}

View File

@@ -1494,6 +1494,16 @@
android:permission="android.permission.BIND_SETTINGS_SUGGESTIONS_SERVICE"
android:exported="true"/>
<service
android:name="com.android.internal.infra.ServiceConnectorTest$TestService"
android:process=":ServiceConnectorRemoteService"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BIND_TEST_SERVICE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
<provider android:name="android.app.activity.LocalProvider"
android:authorities="com.android.frameworks.coretests.LocalProvider">
<meta-data android:name="com.android.frameworks.coretests.string" android:value="foo" />

View File

@@ -0,0 +1,21 @@
/*
* Copyright (C) 2022 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.frameworks.coretests.aidl;
interface ITestServiceConnectorService {
void crashProcess();
}

View File

@@ -0,0 +1,242 @@
/*
* Copyright (C) 2022 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.internal.infra;
import static com.google.common.truth.Truth.assertThat;
import static java.util.Objects.requireNonNull;
import android.annotation.Nullable;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.os.Process;
import android.os.UserHandle;
import androidx.annotation.NonNull;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.android.frameworks.coretests.aidl.ITestServiceConnectorService;
import com.android.internal.infra.ServiceConnectorTest.CapturingServiceLifecycleCallbacks.ServiceLifeCycleEvent;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Unit tests for {@link ServiceConnector}
*/
@RunWith(AndroidJUnit4.class)
public class ServiceConnectorTest {
private final CapturingServiceLifecycleCallbacks mCapturingServiceLifecycleCallbacks =
new CapturingServiceLifecycleCallbacks();
private ServiceConnector<ITestServiceConnectorService> mServiceConnector;
@Before
public void setup() {
Context context = InstrumentationRegistry.getInstrumentation().getContext();
Intent testServiceConnectorServiceIntent = new Intent(TestService.ACTION_TEST_SERVICE);
testServiceConnectorServiceIntent.setPackage(context.getPackageName());
ServiceConnector.Impl<ITestServiceConnectorService> serviceConnector =
new ServiceConnector.Impl<ITestServiceConnectorService>(
context,
testServiceConnectorServiceIntent,
/* bindingFlags= */ 0,
UserHandle.myUserId(),
ITestServiceConnectorService.Stub::asInterface);
serviceConnector.setServiceLifecycleCallbacks(mCapturingServiceLifecycleCallbacks);
mServiceConnector = serviceConnector;
}
@Test
public void connect_invokesLifecycleCallbacks() throws Exception {
connectAndWaitForDone();
assertThat(mCapturingServiceLifecycleCallbacks.getCapturedLifecycleEvents())
.containsExactly(ServiceLifeCycleEvent.ON_CONNECTED)
.inOrder();
}
@Test
public void connect_alreadyConnected_invokesLifecycleCallbacksOnce() throws Exception {
connectAndWaitForDone();
connectAndWaitForDone();
assertThat(mCapturingServiceLifecycleCallbacks.getCapturedLifecycleEvents())
.containsExactly(ServiceLifeCycleEvent.ON_CONNECTED)
.inOrder();
}
@Test
public void unbind_neverConnected_noLifecycleCallbacks() {
unbindAndWaitForDone();
assertThat(mCapturingServiceLifecycleCallbacks.getCapturedLifecycleEvents())
.isEmpty();
}
@Test
public void unbind_whileConnected_invokesLifecycleCallbacks() throws Exception {
connectAndWaitForDone();
unbindAndWaitForDone();
assertThat(mCapturingServiceLifecycleCallbacks.getCapturedLifecycleEvents())
.containsExactly(
ServiceLifeCycleEvent.ON_CONNECTED,
ServiceLifeCycleEvent.ON_DISCONNECTED)
.inOrder();
}
@Test
public void unbind_alreadyUnbound_invokesLifecycleCallbacks() throws Exception {
connectAndWaitForDone();
unbindAndWaitForDone();
unbindAndWaitForDone();
assertThat(mCapturingServiceLifecycleCallbacks.getCapturedLifecycleEvents())
.containsExactly(
ServiceLifeCycleEvent.ON_CONNECTED,
ServiceLifeCycleEvent.ON_DISCONNECTED)
.inOrder();
}
@Test
public void binds_connectsAndUnbindsMultipleTimes_invokesLifecycleCallbacks() throws Exception {
connectAndWaitForDone();
unbindAndWaitForDone();
connectAndWaitForDone();
unbindAndWaitForDone();
connectAndWaitForDone();
assertThat(mCapturingServiceLifecycleCallbacks.getCapturedLifecycleEvents())
.containsExactly(
ServiceLifeCycleEvent.ON_CONNECTED,
ServiceLifeCycleEvent.ON_DISCONNECTED,
ServiceLifeCycleEvent.ON_CONNECTED,
ServiceLifeCycleEvent.ON_DISCONNECTED,
ServiceLifeCycleEvent.ON_CONNECTED)
.inOrder();
}
@Test
public void processCrashes_whileConnected_invokesLifecycleCallbacks() throws Exception {
connectAndWaitForDone();
waitForDone(mServiceConnector.post(service -> service.crashProcess()));
assertThat(mCapturingServiceLifecycleCallbacks.getCapturedLifecycleEvents())
.containsExactly(
ServiceLifeCycleEvent.ON_CONNECTED,
ServiceLifeCycleEvent.ON_BINDER_DIED)
.inOrder();
}
private void connectAndWaitForDone() {
waitForDone(mServiceConnector.connect());
}
private void unbindAndWaitForDone() {
mServiceConnector.unbind();
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
private static void waitForDone(AndroidFuture<?> androidFuture) {
if (androidFuture.isDone()) {
return;
}
try {
androidFuture.get(10, TimeUnit.SECONDS);
} catch (InterruptedException | TimeoutException ex) {
throw new RuntimeException(ex);
} catch (ExecutionException | CancellationException ex) {
// Failed and canceled futures are completed
return;
}
}
public static final class CapturingServiceLifecycleCallbacks implements
ServiceConnector.ServiceLifecycleCallbacks<ITestServiceConnectorService> {
public enum ServiceLifeCycleEvent {
ON_CONNECTED,
ON_DISCONNECTED,
ON_BINDER_DIED,
}
private final ArrayList<ServiceLifeCycleEvent> mCapturedLifecycleEventServices =
new ArrayList<>();
public ArrayList<ServiceLifeCycleEvent> getCapturedLifecycleEvents() {
return mCapturedLifecycleEventServices;
}
@Override
public void onConnected(@NonNull ITestServiceConnectorService service) {
requireNonNull(service);
mCapturedLifecycleEventServices.add(ServiceLifeCycleEvent.ON_CONNECTED);
}
@Override
public void onDisconnected(@NonNull ITestServiceConnectorService service) {
requireNonNull(service);
mCapturedLifecycleEventServices.add(ServiceLifeCycleEvent.ON_DISCONNECTED);
}
@Override
public void onBinderDied() {
mCapturedLifecycleEventServices.add(ServiceLifeCycleEvent.ON_BINDER_DIED);
}
}
public static final class TestService extends Service {
public static String ACTION_TEST_SERVICE = "android.intent.action.BIND_TEST_SERVICE";
@Nullable
@Override
public IBinder onBind(@Nullable Intent intent) {
if (intent == null) {
return null;
}
if (!intent.getAction().equals(ACTION_TEST_SERVICE)) {
return null;
}
return new TestServiceConnectorService().asBinder();
}
}
private static final class TestServiceConnectorService extends
ITestServiceConnectorService.Stub {
@Override
public void crashProcess() {
Process.killProcess(Process.myPid());
}
}
}

View File

@@ -50,6 +50,7 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.infra.AndroidFuture;
import com.android.internal.infra.ServiceConnector;
import com.android.internal.infra.ServiceConnector.ServiceLifecycleCallbacks;
import com.android.server.wm.WindowManagerInternal;
import com.android.server.wm.WindowManagerInternal.TaskSystemBarsListener;
import com.android.server.wm.WindowManagerService;
@@ -64,6 +65,40 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
private static final int CREATE_GAME_SESSION_TIMEOUT_MS = 10_000;
private static final boolean DEBUG = false;
private final ServiceLifecycleCallbacks<IGameService> mGameServiceLifecycleCallbacks =
new ServiceLifecycleCallbacks<IGameService>() {
@Override
public void onConnected(@NonNull IGameService service) {
try {
service.connected(mGameServiceController);
} catch (RemoteException ex) {
Slog.w(TAG, "Failed to send connected event", ex);
}
}
@Override
public void onDisconnected(@NonNull IGameService service) {
try {
service.disconnected();
} catch (RemoteException ex) {
Slog.w(TAG, "Failed to send disconnected event", ex);
}
}
};
private final ServiceLifecycleCallbacks<IGameSessionService>
mGameSessionServiceLifecycleCallbacks =
new ServiceLifecycleCallbacks<IGameSessionService>() {
@Override
public void onBinderDied() {
mBackgroundExecutor.execute(() -> {
synchronized (mLock) {
destroyAndClearAllGameSessionsLocked();
}
});
}
};
private final TaskSystemBarsListener mTaskSystemBarsVisibilityListener =
new TaskSystemBarsListener() {
@Override
@@ -206,11 +241,11 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
}
mIsRunning = true;
// TODO(b/204503192): In cases where the connection to the game service fails retry with
// back off mechanism.
AndroidFuture<Void> unusedPostConnectedFuture = mGameServiceConnector.post(gameService -> {
gameService.connected(mGameServiceController);
});
mGameServiceConnector.setServiceLifecycleCallbacks(mGameServiceLifecycleCallbacks);
mGameSessionServiceConnector.setServiceLifecycleCallbacks(
mGameSessionServiceLifecycleCallbacks);
AndroidFuture<?> unusedConnectFuture = mGameServiceConnector.connect();
try {
mActivityTaskManager.registerTaskStackListener(mTaskStackListener);
@@ -237,19 +272,14 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
mWindowManagerInternal.unregisterTaskSystemBarsListener(
mTaskSystemBarsVisibilityListener);
for (GameSessionRecord gameSessionRecord : mGameSessions.values()) {
destroyGameSessionFromRecordLocked(gameSessionRecord);
}
mGameSessions.clear();
destroyAndClearAllGameSessionsLocked();
// TODO(b/204503192): It is possible that the game service is disconnected. In this
// case we should avoid rebinding just to shut it down again.
mGameServiceConnector.post(gameService -> {
gameService.disconnected();
}).whenComplete((result, t) -> {
mGameServiceConnector.unbind();
});
mGameServiceConnector.unbind();
mGameSessionServiceConnector.unbind();
mGameServiceConnector.setServiceLifecycleCallbacks(null);
mGameSessionServiceConnector.setServiceLifecycleCallbacks(null);
}
private void onTaskCreated(int taskId, @NonNull ComponentName componentName) {
@@ -488,6 +518,14 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
createGameSessionResult.getSurfacePackage()));
}
@GuardedBy("mLock")
private void destroyAndClearAllGameSessionsLocked() {
for (GameSessionRecord gameSessionRecord : mGameSessions.values()) {
destroyGameSessionFromRecordLocked(gameSessionRecord);
}
mGameSessions.clear();
}
private void destroyGameSessionDuringAttach(
int taskId,
CreateGameSessionResult createGameSessionResult) {
@@ -544,9 +582,7 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
Slog.d(TAG, "No active game sessions. Disconnecting GameSessionService");
}
if (mGameSessionServiceConnector != null) {
mGameSessionServiceConnector.unbind();
}
mGameSessionServiceConnector.unbind();
}
}

View File

@@ -17,6 +17,7 @@
package com.android.server.app;
import android.annotation.Nullable;
import android.os.IInterface;
import com.android.internal.infra.AndroidFuture;
@@ -30,9 +31,10 @@ import java.util.concurrent.CompletableFuture;
* Tests provide a service instance via {@link #FakeServiceConnector(IInterface)} that will be
* connected to and used to fulfill service jobs.
*/
final class FakeServiceConnector<T extends IInterface> implements
ServiceConnector<T> {
final class FakeServiceConnector<T extends IInterface> implements ServiceConnector<T> {
private final T mService;
@Nullable
private ServiceLifecycleCallbacks mServiceLifecycleCallbacks;
private boolean mIsConnected;
private int mConnectCount = 0;
@@ -96,9 +98,17 @@ final class FakeServiceConnector<T extends IInterface> implements
@Override
public void unbind() {
if (mServiceLifecycleCallbacks != null) {
mServiceLifecycleCallbacks.onDisconnected(mService);
}
mIsConnected = false;
}
@Override
public void setServiceLifecycleCallbacks(@Nullable ServiceLifecycleCallbacks<T> callbacks) {
mServiceLifecycleCallbacks = callbacks;
}
private void markPossibleConnection() {
if (mIsConnected) {
return;
@@ -106,6 +116,21 @@ final class FakeServiceConnector<T extends IInterface> implements
mConnectCount += 1;
mIsConnected = true;
if (mServiceLifecycleCallbacks != null) {
mServiceLifecycleCallbacks.onConnected(mService);
}
}
public void killServiceProcess() {
if (!mIsConnected) {
return;
}
mIsConnected = false;
if (mServiceLifecycleCallbacks != null) {
mServiceLifecycleCallbacks.onBinderDied();
}
}
/**

View File

@@ -712,6 +712,35 @@ public final class GameServiceProviderInstanceImplTest {
assertThrows(SecurityException.class, () -> mFakeGameService.requestCreateGameSession(10));
}
@Test
public void gameSessionServiceDies_severalActiveGameSessions_destroysGameSessions() {
mGameServiceProviderInstance.start();
startTask(10, GAME_A_MAIN_ACTIVITY);
mockPermissionGranted(Manifest.permission.MANAGE_GAME_ACTIVITY);
mFakeGameService.requestCreateGameSession(10);
FakeGameSession gameSession10 = new FakeGameSession();
SurfacePackage mockSurfacePackage10 = Mockito.mock(SurfacePackage.class);
mFakeGameSessionService.removePendingFutureForTaskId(10)
.complete(new CreateGameSessionResult(gameSession10, mockSurfacePackage10));
startTask(11, GAME_A_MAIN_ACTIVITY);
mFakeGameService.requestCreateGameSession(11);
FakeGameSession gameSession11 = new FakeGameSession();
SurfacePackage mockSurfacePackage11 = Mockito.mock(SurfacePackage.class);
mFakeGameSessionService.removePendingFutureForTaskId(11)
.complete(new CreateGameSessionResult(gameSession11, mockSurfacePackage11));
mFakeGameSessionServiceConnector.killServiceProcess();
assertThat(gameSession10.mIsDestroyed).isTrue();
assertThat(gameSession11.mIsDestroyed).isTrue();
assertThat(mFakeGameServiceConnector.getIsConnected()).isTrue();
assertThat(mFakeGameSessionServiceConnector.getIsConnected()).isFalse();
}
@Test
public void stop_severalActiveGameSessions_destroysGameSessionsAndUnbinds() throws Exception {
mGameServiceProviderInstance.start();