diff --git a/services/CHANGELOG.md b/services/CHANGELOG.md index b52180a8e..8a4604b11 100644 --- a/services/CHANGELOG.md +++ b/services/CHANGELOG.md @@ -6,6 +6,9 @@ **Bug Fixes** +* Make ShellExecutor more resilient to low memory conditions by binding to + androidx.test.services.SpeakEasyService + **New Features** **Breaking Changes** diff --git a/services/shellexecutor/java/androidx/test/services/shellexecutor/ShellCommandClient.java b/services/shellexecutor/java/androidx/test/services/shellexecutor/ShellCommandClient.java index 45a788c31..9f74bd09c 100644 --- a/services/shellexecutor/java/androidx/test/services/shellexecutor/ShellCommandClient.java +++ b/services/shellexecutor/java/androidx/test/services/shellexecutor/ShellCommandClient.java @@ -30,6 +30,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** * Client for the ShellCommandExecutor service, to allow an instrumentation to executes a shell @@ -44,6 +45,7 @@ final class ShellCommandClient { private static final String TAG = "ShellCommandClient"; + private static final ConcurrentHashMap stubCache = new ConcurrentHashMap<>(); private ShellCommandClient() { // Should not be initialized @@ -88,25 +90,28 @@ public static synchronized InputStream execOnServer( shellEnv = new HashMap<>(); } - FindResult result; - - try { - result = BlockingFind.getResult(Looper.getMainLooper(), context, secret); - if (!result.found) { - Log.e(TAG, "Couldn't find a published binder"); + Command commandStub = stubCache.get(secret); + if (commandStub == null || !commandStub.asBinder().isBinderAlive()) { + FindResult result; + try { + result = BlockingFind.getResult(Looper.getMainLooper(), context, secret); + if (!result.found) { + Log.e(TAG, "Couldn't find a published binder"); + throw new ClientNotConnected( + "Couldn't find a binder published by androidx.test.services. It is very likely that" + + " androidx.test.services was killed, which is usually due to low memory" + + " conditions. Consider switching to a device with more memory."); + } + } catch (InterruptedException e) { throw new ClientNotConnected( - "Couldn't find a binder published by androidx.test.services. It is very likely that" - + " androidx.test.services was killed, which is usually due to low memory" - + " conditions. Consider switching to a device with more memory."); + "Search for an androidx.test.services binder was interrupted", e); } - } catch (InterruptedException e) { - throw new ClientNotConnected( - "Search for an androidx.test.services binder was interrupted", e); + commandStub = Command.Stub.asInterface(result.binder); + stubCache.put(secret, commandStub); } ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); - Command commandStub = Command.Stub.asInterface(result.binder); // Only use timeout version if timeout is greater than 0 if (timeoutMs > 0L) { // NOTICE: this is not be supported on older versions of the Command server. diff --git a/services/speakeasy/java/androidx/test/services/speakeasy/client/AppConnection.java b/services/speakeasy/java/androidx/test/services/speakeasy/client/AppConnection.java index ef7755a59..fedb21b7f 100644 --- a/services/speakeasy/java/androidx/test/services/speakeasy/client/AppConnection.java +++ b/services/speakeasy/java/androidx/test/services/speakeasy/client/AppConnection.java @@ -16,16 +16,20 @@ package androidx.test.services.speakeasy.client; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.ServiceConnection; import android.os.Build; import android.os.IBinder; +import android.util.Log; import androidx.test.services.speakeasy.SpeakEasyProtocol; import java.security.SecureRandom; import java.util.Random; /** Allows callers to access the speakeasy binder registry when they have a Context. */ public final class AppConnection implements Connection { + private static final String TAG = "AppConnection"; private static final String PACKAGE_NAME = "androidx.test.services"; static final String SERVICE = "androidx.test.services.speakeasy.server.SpeakEasyService"; @@ -45,11 +49,36 @@ public AppConnection(Context context) { this.random = checkNotNull(random); } + private static final ServiceConnection serviceConnection = + new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) {} + + @Override + public void onServiceDisconnected(ComponentName name) {} + }; + private static boolean isBound = false; + + private synchronized void ensureBound() { + if (!isBound) { + try { + isBound = + context.bindService( + makeIntent(), + serviceConnection, + Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT | Context.BIND_ABOVE_CLIENT); + } catch (SecurityException | IllegalArgumentException e) { + Log.w(TAG, "Failed to bind to SpeakEasyService", e); + } + } + } + @Override public void publish(IBinder binder, PublishResultReceiver rr) { checkNotNull(binder); checkNotNull(rr); + ensureBound(); String key = Long.toHexString(random.nextLong()); Intent intent = makeIntent(); intent.putExtras(SpeakEasyProtocol.Publish.asBundle(key, binder, rr)); @@ -60,6 +89,8 @@ public void publish(IBinder binder, PublishResultReceiver rr) { public void find(String key, FindResultReceiver rr) { checkNotNull(key); checkNotNull(rr); + + ensureBound(); Intent intent = makeIntent(); intent.putExtras(SpeakEasyProtocol.Find.asBundle(key, rr)); startForegroundService(context, intent); diff --git a/services/speakeasy/java/androidx/test/services/speakeasy/server/SpeakEasyService.java b/services/speakeasy/java/androidx/test/services/speakeasy/server/SpeakEasyService.java index bf8d9fd91..b2c260d8e 100644 --- a/services/speakeasy/java/androidx/test/services/speakeasy/server/SpeakEasyService.java +++ b/services/speakeasy/java/androidx/test/services/speakeasy/server/SpeakEasyService.java @@ -25,6 +25,7 @@ import android.app.Service; import android.content.Context; import android.content.Intent; +import android.os.Binder; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; @@ -72,7 +73,7 @@ public void onStart(Intent intent, int id) { @Override public int onStartCommand(Intent intent, int flags, int startId) { onStart(intent, startId); - return START_NOT_STICKY; + return START_STICKY; } @Override @@ -108,6 +109,9 @@ public void run() { } private void serveIntent(Intent in, int startId) { + if (in == null || in.getExtras() == null) { + return; + } SpeakEasyProtocol sep = SpeakEasyProtocol.fromBundle(in.getExtras()); if (null == sep) { return; @@ -148,9 +152,14 @@ private void serveIntent(Intent in, int startId) { } } + private final IBinder binder = new Binder(); + @Override public IBinder onBind(Intent i) { - return null; + if (i != null && i.getExtras() != null) { + serveIntent(i, -1); + } + return binder; } private static class DeathCallback implements SpeakEasy.BinderDeathCallback {