diff options
Diffstat (limited to 'third_party/sl4a/src/main/java/com/google/android/mobly/snippet/SnippetRunner.java')
-rw-r--r-- | third_party/sl4a/src/main/java/com/google/android/mobly/snippet/SnippetRunner.java | 199 |
1 files changed, 199 insertions, 0 deletions
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/SnippetRunner.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/SnippetRunner.java new file mode 100644 index 0000000..36d1348 --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/SnippetRunner.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * 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.google.android.mobly.snippet; + +import android.app.Instrumentation; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.os.Process; +import androidx.test.runner.AndroidJUnitRunner; +import com.google.android.mobly.snippet.rpc.AndroidProxy; +import com.google.android.mobly.snippet.util.EmptyTestClass; +import com.google.android.mobly.snippet.util.Log; +import com.google.android.mobly.snippet.util.NotificationIdFactory; +import java.io.IOException; +import java.net.SocketException; +import java.util.Locale; + +/** + * A launcher that starts the snippet server as an instrumentation so that it has access to the + * target app's context. + * + * <p>We have to extend some subclass of {@link androidx.test.runner.AndroidJUnitRunner} because + * snippets are launched with 'am instrument', and snippet APKs need to access {@link + * androidx.test.platform.app.InstrumentationRegistry}. + * + * <p>The launch and communication protocol between snippet and client is versionated and reported + * as follows: + * + * <ul> + * <li>v0 (not reported): + * <ul> + * <li>Launch as Instrumentation with SnippetRunner. + * <li>No protocol-specific messages reported through instrumentation output. + * <li>'stop' action prints 'OK (0 tests)' + * <li>'start' action prints nothing. + * </ul> + * <li>v1.0: New instrumentation output added to track bringup process + * <ul> + * <li>"SNIPPET START, PROTOCOL <major> <minor>" upon snippet start + * <li>"SNIPPET SERVING, PORT <port>" once server is ready + * </ul> + * </ul> + */ +public class SnippetRunner extends AndroidJUnitRunner { + + /** + * Major version of the launch and communication protocol. + * + * <p>Incrementing this means that compatibility with clients using the older version is broken. + * Avoid breaking compatibility unless there is no other choice. + */ + public static final int PROTOCOL_MAJOR_VERSION = 1; + + /** + * Minor version of the launch and communication protocol. + * + * <p>Increment this when new features are added to the launch and communication protocol that + * are backwards compatible with the old protocol and don't break existing clients. + */ + public static final int PROTOCOL_MINOR_VERSION = 0; + + private static final String ARG_ACTION = "action"; + private static final String ARG_PORT = "port"; + + /** + * Values needed to create a notification channel. This applies to versions > O (26). + */ + private static final String NOTIFICATION_CHANNEL_ID = "msl_channel"; + private static final String NOTIFICATION_CHANNEL_DESC = "Channel reserved for mobly-snippet-lib."; + private static final CharSequence NOTIFICATION_CHANNEL_NAME = "msl"; + + private enum Action { + START, + STOP + }; + + private static final int NOTIFICATION_ID = NotificationIdFactory.create(); + + private Bundle mArguments; + private NotificationManager mNotificationManager; + private Notification mNotification; + + @Override + public void onCreate(Bundle arguments) { + mArguments = arguments; + + // First-run static setup + Log.initLogTag(getContext()); + + // First order of business is to report HELLO to instrumentation output. + sendString( + "SNIPPET START, PROTOCOL " + PROTOCOL_MAJOR_VERSION + " " + PROTOCOL_MINOR_VERSION); + + // Prevent this runner from triggering any real JUnit tests in the snippet by feeding it a + // hardcoded empty test class. + mArguments.putString("class", EmptyTestClass.class.getCanonicalName()); + mNotificationManager = + (NotificationManager) + getTargetContext().getSystemService(Context.NOTIFICATION_SERVICE); + super.onCreate(mArguments); + } + + @Override + public void onStart() { + String actionStr = mArguments.getString(ARG_ACTION); + if (actionStr == null) { + throw new IllegalArgumentException("\"--e action <action>\" was not specified"); + } + Action action = Action.valueOf(actionStr.toUpperCase(Locale.ROOT)); + switch (action) { + case START: + String servicePort = mArguments.getString(ARG_PORT); + int port = 0 /* auto chosen */; + if (servicePort != null) { + port = Integer.parseInt(servicePort); + } + startServer(port); + break; + case STOP: + mNotificationManager.cancel(NOTIFICATION_ID); + mNotificationManager.cancelAll(); + super.onStart(); + } + } + + private void startServer(int port) { + AndroidProxy androidProxy = new AndroidProxy(getContext()); + try { + androidProxy.startLocal(port); + } catch (SocketException e) { + if ("Permission denied".equals(e.getMessage())) { + throw new RuntimeException( + "Failed to start server. No permission to create a socket. Does the *MAIN* " + + "app manifest declare the INTERNET permission?", + e); + } + throw new RuntimeException("Failed to start server", e); + } catch (IOException e) { + throw new RuntimeException("Failed to start server", e); + } + createNotification(); + int actualPort = androidProxy.getPort(); + sendString("SNIPPET SERVING, PORT " + actualPort); + Log.i("Snippet server started for process " + Process.myPid() + " on port " + actualPort); + } + + @SuppressWarnings("deprecation") // Depreciated calls needed for versions < O (26) + private void createNotification() { + Notification.Builder builder; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + builder = new Notification.Builder(getTargetContext()); + builder.setSmallIcon(android.R.drawable.btn_star) + .setTicker(null) + .setWhen(System.currentTimeMillis()) + .setContentTitle("Snippet Service"); + mNotification = builder.getNotification(); + } else { + // Create a new channel for notifications. Needed for versions >= O + NotificationChannel channel = new NotificationChannel( + NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT); + channel.setDescription(NOTIFICATION_CHANNEL_DESC); + mNotificationManager.createNotificationChannel(channel); + + // Build notification + builder = new Notification.Builder(getTargetContext(), NOTIFICATION_CHANNEL_ID); + builder.setSmallIcon(android.R.drawable.btn_star) + .setTicker(null) + .setWhen(System.currentTimeMillis()) + .setContentTitle("Snippet Service"); + mNotification = builder.build(); + } + mNotification.flags = Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT; + mNotificationManager.notify(NOTIFICATION_ID, mNotification); + } + + private void sendString(String string) { + Log.i("Sending protocol message: " + string); + Bundle bundle = new Bundle(); + bundle.putString(Instrumentation.REPORT_KEY_STREAMRESULT, string + "\n"); + sendStatus(0, bundle); + } +} |