1 /* 2 * Copyright (C) 2016 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 package com.google.android.mobly.snippet; 17 18 import android.app.Instrumentation; 19 import android.app.Notification; 20 import android.app.NotificationChannel; 21 import android.app.NotificationManager; 22 import android.content.Context; 23 import android.os.Build; 24 import android.os.Bundle; 25 import android.os.Process; 26 import androidx.test.runner.AndroidJUnitRunner; 27 import com.google.android.mobly.snippet.rpc.AndroidProxy; 28 import com.google.android.mobly.snippet.util.EmptyTestClass; 29 import com.google.android.mobly.snippet.util.Log; 30 import com.google.android.mobly.snippet.util.NotificationIdFactory; 31 import java.io.IOException; 32 import java.net.SocketException; 33 import java.util.Locale; 34 35 /** 36 * A launcher that starts the snippet server as an instrumentation so that it has access to the 37 * target app's context. 38 * 39 * <p>We have to extend some subclass of {@link androidx.test.runner.AndroidJUnitRunner} because 40 * snippets are launched with 'am instrument', and snippet APKs need to access {@link 41 * androidx.test.platform.app.InstrumentationRegistry}. 42 * 43 * <p>The launch and communication protocol between snippet and client is versionated and reported 44 * as follows: 45 * 46 * <ul> 47 * <li>v0 (not reported): 48 * <ul> 49 * <li>Launch as Instrumentation with SnippetRunner. 50 * <li>No protocol-specific messages reported through instrumentation output. 51 * <li>'stop' action prints 'OK (0 tests)' 52 * <li>'start' action prints nothing. 53 * </ul> 54 * <li>v1.0: New instrumentation output added to track bringup process 55 * <ul> 56 * <li>"SNIPPET START, PROTOCOL <major> <minor>" upon snippet start 57 * <li>"SNIPPET SERVING, PORT <port>" once server is ready 58 * </ul> 59 * </ul> 60 */ 61 public class SnippetRunner extends AndroidJUnitRunner { 62 63 /** 64 * Major version of the launch and communication protocol. 65 * 66 * <p>Incrementing this means that compatibility with clients using the older version is broken. 67 * Avoid breaking compatibility unless there is no other choice. 68 */ 69 public static final int PROTOCOL_MAJOR_VERSION = 1; 70 71 /** 72 * Minor version of the launch and communication protocol. 73 * 74 * <p>Increment this when new features are added to the launch and communication protocol that 75 * are backwards compatible with the old protocol and don't break existing clients. 76 */ 77 public static final int PROTOCOL_MINOR_VERSION = 0; 78 79 private static final String ARG_ACTION = "action"; 80 private static final String ARG_PORT = "port"; 81 82 /** 83 * Values needed to create a notification channel. This applies to versions > O (26). 84 */ 85 private static final String NOTIFICATION_CHANNEL_ID = "msl_channel"; 86 private static final String NOTIFICATION_CHANNEL_DESC = "Channel reserved for mobly-snippet-lib."; 87 private static final CharSequence NOTIFICATION_CHANNEL_NAME = "msl"; 88 89 private enum Action { 90 START, 91 STOP 92 }; 93 94 private static final int NOTIFICATION_ID = NotificationIdFactory.create(); 95 96 private Bundle mArguments; 97 private NotificationManager mNotificationManager; 98 private Notification mNotification; 99 100 @Override onCreate(Bundle arguments)101 public void onCreate(Bundle arguments) { 102 mArguments = arguments; 103 104 // First-run static setup 105 Log.initLogTag(getContext()); 106 107 // First order of business is to report HELLO to instrumentation output. 108 sendString( 109 "SNIPPET START, PROTOCOL " + PROTOCOL_MAJOR_VERSION + " " + PROTOCOL_MINOR_VERSION); 110 111 // Prevent this runner from triggering any real JUnit tests in the snippet by feeding it a 112 // hardcoded empty test class. 113 mArguments.putString("class", EmptyTestClass.class.getCanonicalName()); 114 mNotificationManager = 115 (NotificationManager) 116 getTargetContext().getSystemService(Context.NOTIFICATION_SERVICE); 117 super.onCreate(mArguments); 118 } 119 120 @Override onStart()121 public void onStart() { 122 String actionStr = mArguments.getString(ARG_ACTION); 123 if (actionStr == null) { 124 throw new IllegalArgumentException("\"--e action <action>\" was not specified"); 125 } 126 Action action = Action.valueOf(actionStr.toUpperCase(Locale.ROOT)); 127 switch (action) { 128 case START: 129 String servicePort = mArguments.getString(ARG_PORT); 130 int port = 0 /* auto chosen */; 131 if (servicePort != null) { 132 port = Integer.parseInt(servicePort); 133 } 134 startServer(port); 135 break; 136 case STOP: 137 mNotificationManager.cancel(NOTIFICATION_ID); 138 mNotificationManager.cancelAll(); 139 super.onStart(); 140 } 141 } 142 startServer(int port)143 private void startServer(int port) { 144 AndroidProxy androidProxy = new AndroidProxy(getContext()); 145 try { 146 androidProxy.startLocal(port); 147 } catch (SocketException e) { 148 if ("Permission denied".equals(e.getMessage())) { 149 throw new RuntimeException( 150 "Failed to start server. No permission to create a socket. Does the *MAIN* " 151 + "app manifest declare the INTERNET permission?", 152 e); 153 } 154 throw new RuntimeException("Failed to start server", e); 155 } catch (IOException e) { 156 throw new RuntimeException("Failed to start server", e); 157 } 158 createNotification(); 159 int actualPort = androidProxy.getPort(); 160 sendString("SNIPPET SERVING, PORT " + actualPort); 161 Log.i("Snippet server started for process " + Process.myPid() + " on port " + actualPort); 162 } 163 164 @SuppressWarnings("deprecation") // Depreciated calls needed for versions < O (26) createNotification()165 private void createNotification() { 166 Notification.Builder builder; 167 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { 168 builder = new Notification.Builder(getTargetContext()); 169 builder.setSmallIcon(android.R.drawable.btn_star) 170 .setTicker(null) 171 .setWhen(System.currentTimeMillis()) 172 .setContentTitle("Snippet Service"); 173 mNotification = builder.getNotification(); 174 } else { 175 // Create a new channel for notifications. Needed for versions >= O 176 NotificationChannel channel = new NotificationChannel( 177 NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT); 178 channel.setDescription(NOTIFICATION_CHANNEL_DESC); 179 mNotificationManager.createNotificationChannel(channel); 180 181 // Build notification 182 builder = new Notification.Builder(getTargetContext(), NOTIFICATION_CHANNEL_ID); 183 builder.setSmallIcon(android.R.drawable.btn_star) 184 .setTicker(null) 185 .setWhen(System.currentTimeMillis()) 186 .setContentTitle("Snippet Service"); 187 mNotification = builder.build(); 188 } 189 mNotification.flags = Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT; 190 mNotificationManager.notify(NOTIFICATION_ID, mNotification); 191 } 192 sendString(String string)193 private void sendString(String string) { 194 Log.i("Sending protocol message: " + string); 195 Bundle bundle = new Bundle(); 196 bundle.putString(Instrumentation.REPORT_KEY_STREAMRESULT, string + "\n"); 197 sendStatus(0, bundle); 198 } 199 } 200