• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 &lt;major&gt; &lt;minor&gt;" upon snippet start
57  *         <li>"SNIPPET SERVING, PORT &lt;port&gt;" 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