diff options
Diffstat (limited to 'third_party/sl4a/src/main/java/com/google/android')
29 files changed, 2842 insertions, 0 deletions
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/Snippet.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/Snippet.java new file mode 100644 index 0000000..646a3d9 --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/Snippet.java @@ -0,0 +1,23 @@ +/* + * 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; + +public interface Snippet { + /** Invoked when the receiver is shut down. */ + default void shutdown() throws Exception {} + ; +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/SnippetObjectConverter.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/SnippetObjectConverter.java new file mode 100644 index 0000000..b9a101b --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/SnippetObjectConverter.java @@ -0,0 +1,39 @@ +package com.google.android.mobly.snippet; + +import java.lang.reflect.Type; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Interface for a converter that serializes and de-serializes objects. + * + * <p>Classes implementing this interface are meant to provide custom serialization/de-serialization + * logic for complex types. + * + * <p>Serialization here means converting a Java object to {@link JSONObject}, which can be + * transported over Snippet's Rpc protocol. De-serialization is this process in reverse. + */ +public interface SnippetObjectConverter { + /** + * Serializes a complex type object to {@link JSONObject}. + * + * <p>Return null to signify the complex type is not supported. + * + * @param object The object to convert to "serialize". + * @return A JSONObject representation of the input object, or `null` if the input object type + * is not supported. + * @throws JSONException + */ + JSONObject serialize(Object object) throws JSONException; + + /** + * Deserializes a {@link JSONObject} to a Java complex type object. + * + * @param jsonObject A {@link JSONObject} passed from the Rpc client. + * @param type The expected {@link Type} of the Java object. + * @return A Java object of the specified {@link Type}, or `null` if the {@link Type} is not + * supported. + * @throws JSONException + */ + Object deserialize(JSONObject jsonObject, Type type) throws JSONException; +} 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); + } +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/EventCache.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/EventCache.java new file mode 100644 index 0000000..a3106f5 --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/EventCache.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2017 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.event; + +import com.google.android.mobly.snippet.util.Log; +import java.util.Deque; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.LinkedBlockingDeque; + +/** + * Manage the event queue. + * + * <p>EventCache APIs interact with the SnippetEvent cache - a data structure that holds {@link + * SnippetEvent} objects posted from snippet classes. The SnippetEvent cache provides a useful means + * of recording background events (such as sensor data) when the phone is busy with foreground + * activities. + */ +public class EventCache { + private static final String EVENT_DEQUE_ID_TEMPLATE = "%s|%s"; + private static final int EVENT_DEQUE_MAX_SIZE = 1024; + + // A Map with each value being the queue for a particular type of event, and the key being the + // unique ID of the queue. The ID is composed of a callback ID and an event's name. + private final Map<String, LinkedBlockingDeque<SnippetEvent>> mEventDeques = new HashMap<>(); + + private static volatile EventCache mEventCache; + + private EventCache() {} + + public static EventCache getInstance() { + if (mEventCache == null) { + synchronized (EventCache.class) { + if (mEventCache == null) { + mEventCache = new EventCache(); + } + } + } + return mEventCache; + } + + public static String getQueueId(String callbackId, String name) { + return String.format(Locale.US, EVENT_DEQUE_ID_TEMPLATE, callbackId, name); + } + + public LinkedBlockingDeque<SnippetEvent> getEventDeque(String qId) { + synchronized (mEventDeques) { + LinkedBlockingDeque<SnippetEvent> eventDeque = mEventDeques.get(qId); + if (eventDeque == null) { + eventDeque = new LinkedBlockingDeque<>(EVENT_DEQUE_MAX_SIZE); + mEventDeques.put(qId, eventDeque); + } + return eventDeque; + } + } + + /** + * Post an {@link SnippetEvent} object to the Event cache. + * + * <p>Snippet classes should use this method to post events. If EVENT_DEQUE_MAX_SIZE is reached, + * the oldest elements will be retired until the new event could be posted. + * + * @param snippetEvent The snippetEvent to post to {@link EventCache}. + */ + public void postEvent(SnippetEvent snippetEvent) { + String qId = getQueueId(snippetEvent.getCallbackId(), snippetEvent.getName()); + Deque<SnippetEvent> q = getEventDeque(qId); + synchronized (q) { + while (!q.offer(snippetEvent)) { + SnippetEvent retiredEvent = q.removeFirst(); + Log.v( + String.format( + Locale.US, + "Retired event %s due to deque reaching the size limit (%s).", + retiredEvent, + EVENT_DEQUE_MAX_SIZE)); + } + } + Log.v(String.format(Locale.US, "Posted event(%s)", qId)); + } + + /** Clears all cached events. */ + public void clearAll() { + synchronized (mEventDeques) { + mEventDeques.clear(); + } + } +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/EventSnippet.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/EventSnippet.java new file mode 100644 index 0000000..4cebb74 --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/EventSnippet.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2017 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.event; + +import androidx.annotation.Nullable; +import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.rpc.Rpc; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import org.json.JSONException; +import org.json.JSONObject; + +public class EventSnippet implements Snippet { + private static class EventSnippetException extends Exception { + private static final long serialVersionUID = 1L; + + public EventSnippetException(String msg) { + super(msg); + } + } + + private static final int DEFAULT_TIMEOUT_MILLISECOND = 60 * 1000; + private final EventCache mEventCache = EventCache.getInstance(); + + @Rpc( + description = + "Blocks until an event of a specified type has been received. The returned event is removed from the cache. Default timeout is 60s.") + public JSONObject eventWaitAndGet( + String callbackId, String eventName, @Nullable Integer timeout) + throws InterruptedException, JSONException, EventSnippetException { + // The server side should never wait forever, so we'll use a default timeout is one is not + // provided. + if (timeout == null) { + timeout = DEFAULT_TIMEOUT_MILLISECOND; + } + String qId = EventCache.getQueueId(callbackId, eventName); + LinkedBlockingDeque<SnippetEvent> q = mEventCache.getEventDeque(qId); + SnippetEvent result = q.pollFirst(timeout, TimeUnit.MILLISECONDS); + if (result == null) { + throw new EventSnippetException("timeout."); + } + return result.toJson(); + } + + @Rpc( + description = + "Gets and removes all the events of a certain name that have been received so far. " + + "Non-blocking. Potentially racey since it does not guarantee no event of " + + "the same name will occur after the call.") + public List<JSONObject> eventGetAll(String callbackId, String eventName) + throws InterruptedException, JSONException { + String qId = EventCache.getQueueId(callbackId, eventName); + LinkedBlockingDeque<SnippetEvent> q = mEventCache.getEventDeque(qId); + ArrayList<JSONObject> results = new ArrayList<>(q.size()); + ArrayList<SnippetEvent> buffer = new ArrayList<>(q.size()); + q.drainTo(buffer); + for (SnippetEvent snippetEvent : buffer) { + results.add(snippetEvent.toJson()); + } + if (results.size() == 0) { + return Collections.emptyList(); + } + return results; + } + + @Override + public void shutdown() { + mEventCache.clearAll(); + } +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/SnippetEvent.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/SnippetEvent.java new file mode 100644 index 0000000..a90d9eb --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/SnippetEvent.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2017 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.event; + +import android.os.Bundle; +import com.google.android.mobly.snippet.rpc.JsonBuilder; +import org.json.JSONException; +import org.json.JSONObject; + +/** Class used to store information from a callback event. */ +public class SnippetEvent { + + // The ID used to associate an event to a callback object on the client side. + private final String mCallbackId; + // The name of this event, e.g. startXxxServiceOnSuccess. + private final String mName; + // The content of this event. We use Android's Bundle because it adheres to Android convention + // and adding data to it does not throw checked exceptions, which makes the world a better + // place. + private final Bundle mData = new Bundle(); + + private final long mCreationTime; + + /** + * Constructs an {@link SnippetEvent} object. + * + * <p>The object is used to store information from a callback method associated with a call to + * an {@link com.google.android.mobly.snippet.rpc.AsyncRpc} method. + * + * @param callbackId The callbackId passed to the {@link + * com.google.android.mobly.snippet.rpc.AsyncRpc} method. + * @param name The name of the event. + */ + public SnippetEvent(String callbackId, String name) { + if (callbackId == null) { + throw new IllegalArgumentException("SnippetEvent's callback ID shall not be null."); + } + if (name == null) { + throw new IllegalArgumentException("SnippetEvent's name shall not be null."); + } + mCallbackId = callbackId; + mName = name; + mCreationTime = System.currentTimeMillis(); + } + + public String getCallbackId() { + return mCallbackId; + } + + public String getName() { + return mName; + } + + /** + * Get the internal bundle of this event. + * + * <p>This is the only way to add data to the event, because we can't inherit Bundle type and we + * don't want to dup all the getter and setters of {@link Bundle}. + * + * @return The Bundle that holds user data for this {@link SnippetEvent}. + */ + public Bundle getData() { + return mData; + } + + public long getCreationTime() { + return mCreationTime; + } + + public JSONObject toJson() throws JSONException { + JSONObject result = new JSONObject(); + result.put("callbackId", getCallbackId()); + result.put("name", getName()); + result.put("time", getCreationTime()); + result.put("data", JsonBuilder.build(mData)); + return result; + } +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/future/FutureResult.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/future/FutureResult.java new file mode 100644 index 0000000..5e6edd3 --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/future/FutureResult.java @@ -0,0 +1,60 @@ +/* + * 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.future; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** FutureResult represents an eventual execution result for asynchronous operations. */ +public class FutureResult<T> implements Future<T> { + + private final CountDownLatch mLatch = new CountDownLatch(1); + private volatile T mResult = null; + + public void set(T result) { + mResult = result; + mLatch.countDown(); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + @Override + public T get() throws InterruptedException { + mLatch.await(); + return mResult; + } + + @Override + public T get(long timeout, TimeUnit unit) throws InterruptedException { + mLatch.await(timeout, unit); + return mResult; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return mResult != null; + } +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetManager.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetManager.java new file mode 100644 index 0000000..7707de2 --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetManager.java @@ -0,0 +1,290 @@ +/* + * 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.manager; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.SnippetObjectConverter; +import com.google.android.mobly.snippet.event.EventSnippet; +import com.google.android.mobly.snippet.rpc.MethodDescriptor; +import com.google.android.mobly.snippet.rpc.RpcMinSdk; +import com.google.android.mobly.snippet.rpc.RunOnUiThread; +import com.google.android.mobly.snippet.schedulerpc.ScheduleRpcSnippet; +import com.google.android.mobly.snippet.util.Log; +import com.google.android.mobly.snippet.util.MainThread; +import com.google.android.mobly.snippet.util.SnippetLibException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.Callable; + +public class SnippetManager { + /** + * Name of the XML tag specifying what snippet classes to look for RPCs in. + * + * <p>Comma delimited list of full package names for classes that implements the Snippet + * interface. + */ + private static final String TAG_NAME_SNIPPET_LIST = "mobly-snippets"; + /** Name of the XML tag specifying the custom object converter class to use. */ + private static final String TAG_NAME_OBJECT_CONVERTER = "mobly-object-converter"; + + private final Map<Class<? extends Snippet>, Snippet> mSnippets; + /** A map of strings to known RPCs. */ + private final Map<String, MethodDescriptor> mKnownRpcs; + + private static SnippetManager sInstance = null; + private boolean mShutdown = false; + + private SnippetManager(Collection<Class<? extends Snippet>> classList) { + // Synchronized for multiple connections on the same session. Can't use ConcurrentHashMap + // because we have to put in a value of 'null' before the class is constructed, but + // ConcurrentHashMap does not allow null values. + mSnippets = Collections.synchronizedMap(new HashMap<Class<? extends Snippet>, Snippet>()); + Map<String, MethodDescriptor> knownRpcs = new HashMap<>(); + for (Class<? extends Snippet> receiverClass : classList) { + mSnippets.put(receiverClass, null); + Collection<MethodDescriptor> methodList = MethodDescriptor.collectFrom(receiverClass); + for (MethodDescriptor m : methodList) { + if (knownRpcs.containsKey(m.getName())) { + // We already know an RPC of the same name. We don't catch this anywhere because + // this is a programming error. + throw new RuntimeException( + "An RPC with the name " + m.getName() + " is already known."); + } + knownRpcs.put(m.getName(), m); + } + } + // Does not need to be concurrent because this map is read only, so it is safe to access + // from multiple threads. Wrap in an unmodifiableMap to enforce this. + mKnownRpcs = Collections.unmodifiableMap(knownRpcs); + } + + public static synchronized SnippetManager initSnippetManager(Context context) { + if (sInstance != null) { + throw new IllegalStateException("SnippetManager should not be re-initialized"); + } + // Add custom object converter if user provided one. + Class<? extends SnippetObjectConverter> converterClazz = + findSnippetObjectConverterFromMetadata(context); + if (converterClazz != null) { + Log.d("Found custom converter class, adding..."); + SnippetObjectConverterManager.addConverter(converterClazz); + } + Collection<Class<? extends Snippet>> classList = findSnippetClassesFromMetadata(context); + sInstance = new SnippetManager(classList); + return sInstance; + } + + public static SnippetManager getInstance() { + if (sInstance == null) { + throw new IllegalStateException("getInstance() called before init()"); + } + if (sInstance.isShutdown()) { + throw new IllegalStateException("shutdown() called before getInstance()"); + } + return sInstance; + } + + public MethodDescriptor getMethodDescriptor(String methodName) { + return mKnownRpcs.get(methodName); + } + + public SortedSet<String> getMethodNames() { + return new TreeSet<>(mKnownRpcs.keySet()); + } + + public Object invoke(Class<? extends Snippet> clazz, Method method, Object[] args) + throws Throwable { + if (method.isAnnotationPresent(RpcMinSdk.class)) { + int requiredSdkLevel = method.getAnnotation(RpcMinSdk.class).value(); + if (Build.VERSION.SDK_INT < requiredSdkLevel) { + throw new SnippetLibException( + String.format( + Locale.US, + "%s requires API level %d, current level is %d", + method.getName(), + requiredSdkLevel, + Build.VERSION.SDK_INT)); + } + } + Snippet object; + try { + object = get(clazz); + return invoke(object, method, args); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + public void shutdown() throws Exception { + for (final Entry<Class<? extends Snippet>, Snippet> entry : mSnippets.entrySet()) { + if (entry.getValue() == null) { + continue; + } + Method method = entry.getKey().getMethod("shutdown"); + if (method.isAnnotationPresent(RunOnUiThread.class)) { + Log.d("Shutting down " + entry.getKey().getName() + " on the main thread"); + MainThread.run( + new Callable<Void>() { + @Override + public Void call() throws Exception { + entry.getValue().shutdown(); + return null; + } + }); + } else { + Log.d("Shutting down " + entry.getKey().getName()); + entry.getValue().shutdown(); + } + } + mSnippets.clear(); + mKnownRpcs.clear(); + mShutdown = true; + } + + public boolean isShutdown() { + return mShutdown; + } + + private static Bundle findMetadata(Context context) { + ApplicationInfo appInfo; + try { + appInfo = + context.getPackageManager() + .getApplicationInfo( + context.getPackageName(), PackageManager.GET_META_DATA); + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalStateException( + "Failed to find ApplicationInfo with package name: " + + context.getPackageName()); + } + return appInfo.metaData; + } + + private static Class<? extends SnippetObjectConverter> findSnippetObjectConverterFromMetadata( + Context context) { + String className = findMetadata(context).getString(TAG_NAME_OBJECT_CONVERTER); + if (className == null) { + Log.i("No object converter provided."); + return null; + } + try { + return Class.forName(className).asSubclass(SnippetObjectConverter.class); + } catch (ClassNotFoundException | ClassCastException e) { + Log.e("Failed to find class " + className); + throw new RuntimeException(e); + } + } + + private static Set<Class<? extends Snippet>> findSnippetClassesFromMetadata(Context context) { + String snippets = findMetadata(context).getString(TAG_NAME_SNIPPET_LIST); + if (snippets == null) { + throw new IllegalStateException( + "AndroidManifest.xml does not contain a <metadata> tag with " + + "name=\"" + + TAG_NAME_SNIPPET_LIST + + "\""); + } + String[] snippetClassNames = snippets.split("\\s*,\\s*"); + Set<Class<? extends Snippet>> receiverSet = new HashSet<>(); + /** Add the event snippet class which is provided within the Snippet Lib. */ + receiverSet.add(EventSnippet.class); + /** Add the schedule RPC snippet class which is provided within the Snippet Lib. */ + receiverSet.add(ScheduleRpcSnippet.class); + for (String snippetClassName : snippetClassNames) { + try { + Log.i("Trying to load Snippet class: " + snippetClassName); + Class<? extends Snippet> snippetClass = + Class.forName(snippetClassName).asSubclass(Snippet.class); + receiverSet.add(snippetClass); + } catch (ClassNotFoundException | ClassCastException e) { + Log.e("Failed to find class " + snippetClassName); + throw new RuntimeException(e); + } + } + if (receiverSet.isEmpty()) { + throw new IllegalStateException("Found no subclasses of Snippet."); + } + return receiverSet; + } + + private Snippet get(Class<? extends Snippet> clazz) throws Exception { + Snippet snippetImpl = mSnippets.get(clazz); + if (snippetImpl == null) { + // First time calling an RPC for this snippet; construct an instance under lock. + synchronized (clazz) { + snippetImpl = mSnippets.get(clazz); + if (snippetImpl == null) { + final Constructor<? extends Snippet> constructor = clazz.getConstructor(); + if (constructor.isAnnotationPresent(RunOnUiThread.class)) { + Log.d("Constructing " + clazz + " on the main thread"); + snippetImpl = + MainThread.run( + new Callable<Snippet>() { + @Override + public Snippet call() throws Exception { + return constructor.newInstance(); + } + }); + } else { + Log.d("Constructing " + clazz); + snippetImpl = constructor.newInstance(); + } + mSnippets.put(clazz, snippetImpl); + } + } + } + return snippetImpl; + } + + private Object invoke(final Snippet snippetImpl, final Method method, final Object[] args) + throws Exception { + if (method.isAnnotationPresent(RunOnUiThread.class)) { + Log.d( + "Invoking RPC method " + + method.getDeclaringClass() + + "#" + + method.getName() + + " on the main thread"); + return MainThread.run( + new Callable<Object>() { + @Override + public Object call() throws Exception { + return method.invoke(snippetImpl, args); + } + }); + } else { + Log.d("Invoking RPC method " + method.getDeclaringClass() + "#" + method.getName()); + return method.invoke(snippetImpl, args); + } + } +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetObjectConverterManager.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetObjectConverterManager.java new file mode 100644 index 0000000..df8af53 --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetObjectConverterManager.java @@ -0,0 +1,65 @@ +package com.google.android.mobly.snippet.manager; + +import com.google.android.mobly.snippet.SnippetObjectConverter; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Type; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Manager for classes that implement {@link SnippetObjectConverter}. + * + * <p>This class is created to separate how Snippet Lib handles object conversion internally from + * how the conversion scheme for complex types is defined for users. + * + * <p>Snippet Lib can pull in the custom serializers and deserializers through here in various + * stages of execution, whereas users can have a clean interface for supplying these methods without + * worrying about internal states of Snippet Lib. + * + * <p>This gives us the flexibility of changing Snippet Lib internal structure or expanding support + * without impacting users. E.g. we can support multiple converter classes in the future. + */ +public class SnippetObjectConverterManager { + private static SnippetObjectConverter mConverter; + private static volatile SnippetObjectConverterManager mManager; + + private SnippetObjectConverterManager() {} + + public static synchronized SnippetObjectConverterManager getInstance() { + if (mManager == null) { + mManager = new SnippetObjectConverterManager(); + } + return mManager; + } + + static void addConverter(Class<? extends SnippetObjectConverter> converterClass) { + if (mConverter != null) { + throw new RuntimeException("A converter has been added, cannot add again."); + } + try { + mConverter = converterClass.getConstructor().newInstance(); + } catch (NoSuchMethodException e) { + throw new RuntimeException("No default constructor found for the converter class."); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e.getCause()); + } catch (InstantiationException e) { + throw new RuntimeException(e); + } + } + + public Object objectToJson(Object object) throws JSONException { + if (mConverter == null) { + return null; + } + return mConverter.serialize(object); + } + + public Object jsonToObject(JSONObject jsonObject, Type type) throws JSONException { + if (mConverter == null) { + return null; + } + return mConverter.deserialize(jsonObject, type); + } +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AndroidProxy.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AndroidProxy.java new file mode 100644 index 0000000..9428c82 --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AndroidProxy.java @@ -0,0 +1,37 @@ +/* + * 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.rpc; + +import android.content.Context; +import java.io.IOException; + +public class AndroidProxy { + + private final JsonRpcServer mJsonRpcServer; + + public AndroidProxy(Context context) { + mJsonRpcServer = new JsonRpcServer(context); + } + + public void startLocal(int port) throws IOException { + mJsonRpcServer.startLocal(port); + } + + public int getPort() { + return mJsonRpcServer.getPort(); + } +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AsyncRpc.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AsyncRpc.java new file mode 100644 index 0000000..6bdd8ca --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AsyncRpc.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2017 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.rpc; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The {@link AsyncRpc} annotation is used to annotate server-side implementations of RPCs that + * trigger asynchronous events. This behaves generally the same as {@link Rpc}, but methods that are + * annotated with {@link AsyncRpc} are expected to take the extra parameter which is the ID to use + * when posting async events. + * + * <p>Sample Usage: + * + * <pre>{@code + * {@literal @}AsyncRpc(description = "An example showing the usage of AsyncRpc") + * public void doSomethingAsync(String callbackId, ...) { + * // start some async operation and post a Snippet Event object with the given callbackId. + * } + * }</pre> + * + * AsyncRpc methods can still return serializable values, which will be transported in the regular + * return value field of the Rpc protocol. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface AsyncRpc { + /** Returns brief description of the function. Should be limited to one or two sentences. */ + String description(); +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonBuilder.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonBuilder.java new file mode 100644 index 0000000..a1d3425 --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonBuilder.java @@ -0,0 +1,201 @@ +/* + * 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.rpc; + +import android.content.ComponentName; +import android.content.Intent; +import android.os.Bundle; +import android.os.ParcelUuid; +import com.google.android.mobly.snippet.manager.SnippetObjectConverterManager; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public class JsonBuilder { + + private JsonBuilder() {} + + @SuppressWarnings("unchecked") + public static Object build(Object data) throws JSONException { + if (data == null) { + return JSONObject.NULL; + } + if (data instanceof Integer) { + return data; + } + if (data instanceof Float) { + return data; + } + if (data instanceof Double) { + return data; + } + if (data instanceof Long) { + return data; + } + if (data instanceof String) { + return data; + } + if (data instanceof Boolean) { + return data; + } + if (data instanceof JsonSerializable) { + return ((JsonSerializable) data).toJSON(); + } + if (data instanceof JSONObject) { + return data; + } + if (data instanceof JSONArray) { + return data; + } + if (data instanceof Set<?>) { + List<Object> items = new ArrayList<>((Set<?>) data); + return buildJsonList(items); + } + if (data instanceof Collection<?>) { + List<Object> items = new ArrayList<>((Collection<?>) data); + return buildJsonList(items); + } + if (data instanceof List<?>) { + return buildJsonList((List<?>) data); + } + if (data instanceof Bundle) { + return buildJsonBundle((Bundle) data); + } + if (data instanceof Intent) { + return buildJsonIntent((Intent) data); + } + if (data instanceof Map<?, ?>) { + // TODO(damonkohler): I would like to make this a checked cast if possible. + return buildJsonMap((Map<String, ?>) data); + } + if (data instanceof ParcelUuid) { + return data.toString(); + } + // TODO(xpconanfan): Deprecate the following default non-primitive type builders. + if (data instanceof InetSocketAddress) { + return buildInetSocketAddress((InetSocketAddress) data); + } + if (data instanceof InetAddress) { + return buildInetAddress((InetAddress) data); + } + if (data instanceof URL) { + return buildURL((URL) data); + } + if (data instanceof byte[]) { + JSONArray result = new JSONArray(); + for (byte b : (byte[]) data) { + result.put(b & 0xFF); + } + return result; + } + if (data instanceof Object[]) { + return buildJSONArray((Object[]) data); + } + // Try with custom converter provided by user. + Object result = SnippetObjectConverterManager.getInstance().objectToJson(data); + if (result != null) { + return result; + } + return data.toString(); + } + + private static Object buildInetAddress(InetAddress data) { + JSONArray address = new JSONArray(); + address.put(data.getHostName()); + address.put(data.getHostAddress()); + return address; + } + + private static Object buildInetSocketAddress(InetSocketAddress data) { + JSONArray address = new JSONArray(); + address.put(data.getHostName()); + address.put(data.getPort()); + return address; + } + + private static JSONArray buildJSONArray(Object[] data) throws JSONException { + JSONArray result = new JSONArray(); + for (Object o : data) { + result.put(build(o)); + } + return result; + } + + private static JSONObject buildJsonBundle(Bundle bundle) throws JSONException { + JSONObject result = new JSONObject(); + bundle.setClassLoader(JsonBuilder.class.getClassLoader()); + for (String key : bundle.keySet()) { + result.put(key, build(bundle.get(key))); + } + return result; + } + + private static JSONObject buildJsonIntent(Intent data) throws JSONException { + JSONObject result = new JSONObject(); + result.put("data", data.getDataString()); + result.put("type", data.getType()); + result.put("extras", build(data.getExtras())); + result.put("categories", build(data.getCategories())); + result.put("action", data.getAction()); + ComponentName component = data.getComponent(); + if (component != null) { + result.put("packagename", component.getPackageName()); + result.put("classname", component.getClassName()); + } + result.put("flags", data.getFlags()); + return result; + } + + private static <T> JSONArray buildJsonList(final List<T> list) throws JSONException { + JSONArray result = new JSONArray(); + for (T item : list) { + result.put(build(item)); + } + return result; + } + + private static JSONObject buildJsonMap(Map<String, ?> map) throws JSONException { + JSONObject result = new JSONObject(); + for (Entry<String, ?> entry : map.entrySet()) { + String key = entry.getKey(); + if (key == null) { + key = ""; + } + result.put(key, build(entry.getValue())); + } + return result; + } + + private static Object buildURL(URL data) throws JSONException { + JSONObject url = new JSONObject(); + url.put("Authority", data.getAuthority()); + url.put("Host", data.getHost()); + url.put("Path", data.getPath()); + url.put("Port", data.getPort()); + url.put("Protocol", data.getProtocol()); + return url; + } +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcResult.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcResult.java new file mode 100644 index 0000000..90cf8f9 --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcResult.java @@ -0,0 +1,79 @@ +/* + * 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.rpc; + +import java.io.PrintWriter; +import java.io.StringWriter; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Represents a JSON RPC result. + * + * @see <a href="http://json-rpc.org/wiki/specification">http://json-rpc.org/wiki/specification</a> + */ +public class JsonRpcResult { + + private JsonRpcResult() { + // Utility class. + } + + public static JSONObject empty(int id) throws JSONException { + JSONObject json = new JSONObject(); + json.put("id", id); + json.put("result", JSONObject.NULL); + json.put("callback", JSONObject.NULL); + json.put("error", JSONObject.NULL); + return json; + } + + public static JSONObject result(int id, Object data) throws JSONException { + JSONObject json = new JSONObject(); + json.put("id", id); + json.put("result", JsonBuilder.build(data)); + json.put("callback", JSONObject.NULL); + json.put("error", JSONObject.NULL); + return json; + } + + public static JSONObject callback(int id, Object data, String callbackId) throws JSONException { + JSONObject json = new JSONObject(); + json.put("id", id); + json.put("result", JsonBuilder.build(data)); + json.put("callback", callbackId); + json.put("error", JSONObject.NULL); + return json; + } + + public static JSONObject error(int id, Throwable t) throws JSONException { + String stackTrace = getStackTrace(t); + JSONObject json = new JSONObject(); + json.put("id", id); + json.put("result", JSONObject.NULL); + json.put("callback", JSONObject.NULL); + json.put("error", stackTrace); + return json; + } + + public static String getStackTrace(Throwable throwable) { + StringWriter stackTraceWriter = new StringWriter(); + stackTraceWriter.write("\n-------------- Java Stacktrace ---------------\n"); + throwable.printStackTrace(new PrintWriter(stackTraceWriter)); + stackTraceWriter.write("----------------------------------------------"); + return stackTraceWriter.toString(); + } +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcServer.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcServer.java new file mode 100644 index 0000000..1dde423 --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcServer.java @@ -0,0 +1,121 @@ +/* + * 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.rpc; + +import android.content.Context; +import com.google.android.mobly.snippet.manager.SnippetManager; +import com.google.android.mobly.snippet.util.Log; +import com.google.android.mobly.snippet.util.RpcUtil; +import java.io.BufferedReader; +import java.io.PrintWriter; +import java.net.Socket; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** A JSON RPC server that forwards RPC calls to a specified receiver object. */ +public class JsonRpcServer extends SimpleServer { + private static final String CMD_CLOSE_SESSION = "closeSl4aSession"; + private static final String CMD_HELP = "help"; + + private final SnippetManager mSnippetManager; + private final RpcUtil mRpcUtil; + + /** Construct a {@link JsonRpcServer} connected to the provided {@link SnippetManager}. */ + public JsonRpcServer(Context context) { + mSnippetManager = SnippetManager.initSnippetManager(context); + mRpcUtil = new RpcUtil(); + } + + @Override + protected void handleRPCConnection( + Socket sock, Integer UID, BufferedReader reader, PrintWriter writer) throws Exception { + Log.d("UID " + UID); + String data; + while ((data = reader.readLine()) != null) { + Log.v("Session " + UID + " Received: " + data); + JSONObject request = new JSONObject(data); + int id = request.getInt("id"); + String method = request.getString("method"); + JSONArray params = request.getJSONArray("params"); + + // Handle builtin commands + if (method.equals(CMD_HELP)) { + help(writer, id, mSnippetManager, UID); + continue; + } else if (method.equals(CMD_CLOSE_SESSION)) { + Log.d("Got shutdown signal"); + synchronized (writer) { + // Shut down all RPC receivers. + mSnippetManager.shutdown(); + + // Shut down this client connection. As soon as this happens, the client will + // kill us by triggering the 'stop' action from another instrumentation, so no + // other cleanup steps are guaranteed to execute. + send(writer, JsonRpcResult.empty(id), UID); + reader.close(); + writer.close(); + sock.close(); + + // Shut down this server. + shutdown(); + } + return; + } + JSONObject returnValue = mRpcUtil.invokeRpc(method, params, id, UID); + send(writer, returnValue, UID); + } + } + + private void help(PrintWriter writer, int id, SnippetManager receiverManager, Integer UID) + throws JSONException { + // Create a map from class simple name to the methods inside it. + Map<String, Set<MethodDescriptor>> methods = new TreeMap<>(); + for (String method : receiverManager.getMethodNames()) { + MethodDescriptor descriptor = receiverManager.getMethodDescriptor(method); + String snippetClassName = descriptor.getSnippetClass().getSimpleName(); + Set<MethodDescriptor> snippetClassMethods = methods.get(snippetClassName); + if (snippetClassMethods == null) { + // Preserve insertion order (alphabetical) + snippetClassMethods = new LinkedHashSet<>(); + methods.put(snippetClassName, snippetClassMethods); + } + snippetClassMethods.add(descriptor); + } + StringBuilder result = new StringBuilder(); + for (Map.Entry<String, Set<MethodDescriptor>> entry : methods.entrySet()) { + result.append("\nRPCs provided by ").append(entry.getKey()).append(":\n"); + for (MethodDescriptor descriptor : entry.getValue()) { + result.append(" ").append(descriptor.getHelp()).append("\n"); + } + } + send(writer, JsonRpcResult.result(id, result), UID); + } + + private void send(PrintWriter writer, JSONObject result, int UID) { + writer.write(result + "\n"); + writer.flush(); + Log.v("Session " + UID + " Sent: " + result); + } + + @Override + protected void handleConnection(Socket socket) throws Exception {} +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonSerializable.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonSerializable.java new file mode 100644 index 0000000..5871e01 --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonSerializable.java @@ -0,0 +1,24 @@ +/* + * 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.rpc; + +import org.json.JSONException; +import org.json.JSONObject; + +public interface JsonSerializable { + JSONObject toJSON() throws JSONException; +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/MethodDescriptor.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/MethodDescriptor.java new file mode 100644 index 0000000..b9c8a7a --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/MethodDescriptor.java @@ -0,0 +1,252 @@ +/* + * 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.rpc; + +import android.content.Intent; +import android.net.Uri; +import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.manager.SnippetManager; +import com.google.android.mobly.snippet.manager.SnippetObjectConverterManager; +import com.google.android.mobly.snippet.util.AndroidUtil; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** An adapter that wraps {@code Method}. */ +public final class MethodDescriptor { + private final Method mMethod; + private final Class<? extends Snippet> mClass; + + private MethodDescriptor(Class<? extends Snippet> clazz, Method method) { + mClass = clazz; + mMethod = method; + } + + @Override + public String toString() { + return mMethod.getDeclaringClass().getCanonicalName() + "." + mMethod.getName(); + } + + /** Collects all methods with {@code RPC} annotation from given class. */ + public static Collection<MethodDescriptor> collectFrom(Class<? extends Snippet> clazz) { + List<MethodDescriptor> descriptors = new ArrayList<MethodDescriptor>(); + for (Method method : clazz.getMethods()) { + if (method.isAnnotationPresent(Rpc.class) + || method.isAnnotationPresent(AsyncRpc.class)) { + descriptors.add(new MethodDescriptor(clazz, method)); + } + } + return descriptors; + } + + /** + * Invokes the call that belongs to this object with the given parameters. Wraps the response + * (possibly an exception) in a JSONObject. + * + * @param parameters {@code JSONArray} containing the parameters + * @return result + * @throws Throwable the exception raised from executing the RPC method. + */ + public Object invoke(SnippetManager manager, final JSONArray parameters) throws Throwable { + final Type[] parameterTypes = getGenericParameterTypes(); + final Object[] args = new Object[parameterTypes.length]; + + if (parameters.length() > args.length) { + throw new RpcError("Too many parameters specified."); + } + + for (int i = 0; i < args.length; i++) { + final Type parameterType = parameterTypes[i]; + if (i < parameters.length()) { + args[i] = convertParameter(parameters, i, parameterType); + } else { + throw new RpcError("Argument " + (i + 1) + " is not present"); + } + } + + return manager.invoke(mClass, mMethod, args); + } + + /** Converts a parameter from JSON into a Java Object. */ + // TODO(damonkohler): This signature is a bit weird (auto-refactored). The obvious alternative + // would be to work on one supplied parameter and return the converted parameter. However, + // that's problematic because you lose the ability to call the getXXX methods on the JSON array. + // @VisibleForTesting + private static Object convertParameter(final JSONArray parameters, int index, Type type) + throws JSONException, RpcError { + try { + // We must handle null and numbers explicitly because we cannot magically cast them. We + // also need to convert implicitly from numbers to bools. + if (parameters.isNull(index)) { + return null; + } else if (type == Boolean.class || type == boolean.class) { + try { + return parameters.getBoolean(index); + } catch (JSONException e) { + return parameters.getInt(index) != 0; + } + } else if (type == Long.class || type == long.class) { + return parameters.getLong(index); + } else if (type == Double.class || type == double.class) { + return parameters.getDouble(index); + } else if (type == Integer.class || type == int.class) { + return parameters.getInt(index); + } else if (type == Intent.class) { + return buildIntent(parameters.getJSONObject(index)); + } else if (type == String.class) { + return parameters.getString(index); + } else if (type == Integer[].class || type == int[].class) { + JSONArray list = parameters.getJSONArray(index); + Integer[] result = new Integer[list.length()]; + for (int i = 0; i < list.length(); i++) { + result[i] = list.getInt(i); + } + return result; + } else if (type == Long[].class || type == long[].class) { + JSONArray list = parameters.getJSONArray(index); + Long[] result = new Long[list.length()]; + for (int i = 0; i < list.length(); i++) { + result[i] = list.getLong(i); + } + return result; + } else if (type == Byte.class || type == byte[].class) { + JSONArray list = parameters.getJSONArray(index); + byte[] result = new byte[list.length()]; + for (int i = 0; i < list.length(); i++) { + result[i] = (byte) list.getInt(i); + } + return result; + } else if (type == String[].class) { + JSONArray list = parameters.getJSONArray(index); + String[] result = new String[list.length()]; + for (int i = 0; i < list.length(); i++) { + result[i] = list.getString(i); + } + return result; + } else if (type == JSONObject.class) { + return parameters.getJSONObject(index); + } else if (type == JSONArray.class) { + return parameters.getJSONArray(index); + } else { + // Try any custom converter provided. + Object object = + SnippetObjectConverterManager.getInstance() + .jsonToObject(parameters.getJSONObject(index), type); + if (object != null) { + return object; + } + // Magically cast the parameter to the right Java type. + return ((Class<?>) type).cast(parameters.get(index)); + } + } catch (ClassCastException e) { + throw new RpcError( + "Argument " + + (index + 1) + + " should be of type " + + ((Class<?>) type).getSimpleName() + + ", but is of type " + + parameters.get(index).getClass().getSimpleName()); + } + } + + private static Object buildIntent(JSONObject jsonObject) throws JSONException { + Intent intent = new Intent(); + if (jsonObject.has("action")) { + intent.setAction(jsonObject.getString("action")); + } + if (jsonObject.has("data") && jsonObject.has("type")) { + intent.setDataAndType( + Uri.parse(jsonObject.optString("data", null)), + jsonObject.optString("type", null)); + } else if (jsonObject.has("data")) { + intent.setData(Uri.parse(jsonObject.optString("data", null))); + } else if (jsonObject.has("type")) { + intent.setType(jsonObject.optString("type", null)); + } + if (jsonObject.has("packagename") && jsonObject.has("classname")) { + intent.setClassName( + jsonObject.getString("packagename"), jsonObject.getString("classname")); + } + if (jsonObject.has("flags")) { + intent.setFlags(jsonObject.getInt("flags")); + } + if (!jsonObject.isNull("extras")) { + AndroidUtil.putExtrasFromJsonObject(jsonObject.getJSONObject("extras"), intent); + } + if (!jsonObject.isNull("categories")) { + JSONArray categories = jsonObject.getJSONArray("categories"); + for (int i = 0; i < categories.length(); i++) { + intent.addCategory(categories.getString(i)); + } + } + return intent; + } + + public String getName() { + return mMethod.getName(); + } + + private Type[] getGenericParameterTypes() { + return mMethod.getGenericParameterTypes(); + } + + public boolean isAsync() { + return mMethod.isAnnotationPresent(AsyncRpc.class); + } + + Class<? extends Snippet> getSnippetClass() { + return mClass; + } + + private String getAnnotationDescription() { + if (isAsync()) { + AsyncRpc annotation = mMethod.getAnnotation(AsyncRpc.class); + return annotation.description(); + } + Rpc annotation = mMethod.getAnnotation(Rpc.class); + return annotation.description(); + } + /** + * Returns a human-readable help text for this RPC, based on annotations in the source code. + * + * @return derived help string + */ + String getHelp() { + StringBuilder paramBuilder = new StringBuilder(); + Class<?>[] parameterTypes = mMethod.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + if (i != 0) { + paramBuilder.append(", "); + } + paramBuilder.append(parameterTypes[i].getSimpleName()); + } + return String.format( + Locale.US, + "%s %s(%s) returns %s // %s", + isAsync() ? "@AsyncRpc" : "@Rpc", + mMethod.getName(), + paramBuilder, + mMethod.getReturnType().getSimpleName(), + getAnnotationDescription()); + } +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/Rpc.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/Rpc.java new file mode 100644 index 0000000..af321ba --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/Rpc.java @@ -0,0 +1,36 @@ +/* + * 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.rpc; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The {@link Rpc} annotation is used to annotate server-side implementations of RPCs. It describes + * meta-information (currently a brief documentation of the function), and marks a function as the + * implementation of an RPC. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Rpc { + /** Returns brief description of the function. Should be limited to one or two sentences. */ + String description(); +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcError.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcError.java new file mode 100644 index 0000000..0862673 --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcError.java @@ -0,0 +1,25 @@ +/* + * 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.rpc; + +@SuppressWarnings("serial") +public class RpcError extends Exception { + + public RpcError(String message) { + super(message); + } +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcMinSdk.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcMinSdk.java new file mode 100644 index 0000000..f03fd2a --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcMinSdk.java @@ -0,0 +1,29 @@ +/* + * 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.rpc; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Use this annotation to specify minimum SDK level (if higher than 3). */ +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RpcMinSdk { + /** Minimum SDK Level. */ + int value(); +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RunOnUiThread.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RunOnUiThread.java new file mode 100644 index 0000000..cde08f0 --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RunOnUiThread.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2017 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.rpc; + +import com.google.android.mobly.snippet.Snippet; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * This annotation will cause the RPC to execute on the main app thread. + * + * <p>This annotation can be applied to: + * + * <ul> + * <li>The constructor of a class implementing the {@link Snippet} interface. + * <li>A method annotated with the {@link Rpc} or {@link AsyncRpc} annotation. + * <li>The {@link Snippet#shutdown()} method. + * </ul> + */ +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RunOnUiThread {} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/SimpleServer.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/SimpleServer.java new file mode 100644 index 0000000..db7255a --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/SimpleServer.java @@ -0,0 +1,285 @@ +/* + * 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.rpc; + +import com.google.android.mobly.snippet.util.Log; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import org.json.JSONException; +import org.json.JSONObject; + +/** A simple server. */ +public abstract class SimpleServer { + private static int threadIndex = 0; + private final ConcurrentHashMap<Integer, ConnectionThread> mConnectionThreads = + new ConcurrentHashMap<>(); + private final List<SimpleServerObserver> mObservers = new ArrayList<>(); + private volatile boolean mStopServer = false; + private ServerSocket mServer; + private Thread mServerThread; + + public interface SimpleServerObserver { + void onConnect(); + + void onDisconnect(); + } + + protected abstract void handleConnection(Socket socket) throws Exception; + + protected abstract void handleRPCConnection( + Socket socket, Integer UID, BufferedReader reader, PrintWriter writer) throws Exception; + + /** Adds an observer. */ + public void addObserver(SimpleServerObserver observer) { + mObservers.add(observer); + } + + /** Removes an observer. */ + public void removeObserver(SimpleServerObserver observer) { + mObservers.remove(observer); + } + + private void notifyOnConnect() { + for (SimpleServerObserver observer : mObservers) { + observer.onConnect(); + } + } + + private void notifyOnDisconnect() { + for (SimpleServerObserver observer : mObservers) { + observer.onDisconnect(); + } + } + + private final class ConnectionThread extends Thread { + private final Socket mmSocket; + private final BufferedReader reader; + private final PrintWriter writer; + private final Integer UID; + private final boolean isRpc; + + private ConnectionThread( + Socket socket, + boolean rpc, + Integer uid, + BufferedReader reader, + PrintWriter writer) { + setName("SimpleServer ConnectionThread " + getId()); + mmSocket = socket; + this.UID = uid; + this.reader = reader; + this.writer = writer; + this.isRpc = rpc; + } + + @Override + public void run() { + Log.v("Server thread " + getId() + " started."); + try { + if (isRpc) { + Log.d("Handling RPC connection in " + getId()); + handleRPCConnection(mmSocket, UID, reader, writer); + } else { + Log.d("Handling Non-RPC connection in " + getId()); + handleConnection(mmSocket); + } + } catch (Exception e) { + if (!mStopServer) { + Log.e("Server error.", e); + } + } finally { + close(); + mConnectionThreads.remove(this.UID); + notifyOnDisconnect(); + Log.v("Server thread " + getId() + " stopped."); + } + } + + private void close() { + if (mmSocket != null) { + try { + mmSocket.close(); + } catch (IOException e) { + Log.e(e.getMessage(), e); + } + } + } + } + + private InetAddress getPrivateInetAddress() throws UnknownHostException, SocketException { + + InetAddress candidate = null; + Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces(); + for (NetworkInterface netint : Collections.list(nets)) { + if (!netint.isLoopback() || !netint.isUp()) { // Ignore if localhost or not active + continue; + } + Enumeration<InetAddress> addresses = netint.getInetAddresses(); + for (InetAddress address : Collections.list(addresses)) { + if (address instanceof Inet4Address) { + Log.d("local address " + address); + return address; // Prefer ipv4 + } + candidate = address; // Probably an ipv6 + } + } + if (candidate != null) { + return candidate; // return ipv6 address if no suitable ipv6 + } + return InetAddress.getLocalHost(); // No damn matches. Give up, return local host. + } + + /** + * Starts the RPC server bound to the localhost address. + * + * @param port the port to bind to or 0 to pick any unused port + * @throws IOException + */ + public void startLocal(int port) throws IOException { + InetAddress address = getPrivateInetAddress(); + mServer = new ServerSocket(port, 5 /* backlog */, address); + start(); + } + + public int getPort() { + return mServer.getLocalPort(); + } + + private void start() { + mServerThread = + new Thread() { + @Override + public void run() { + while (!mStopServer) { + try { + Socket sock = mServer.accept(); + if (!mStopServer) { + startConnectionThread(sock); + } else { + sock.close(); + } + } catch (IOException e) { + if (!mStopServer) { + Log.e("Failed to accept connection.", e); + } + } catch (JSONException e) { + if (!mStopServer) { + Log.e("Failed to parse request.", e); + } + } + } + } + }; + mServerThread.start(); + Log.v("Bound to " + mServer.getInetAddress()); + } + + private void startConnectionThread(final Socket sock) throws IOException, JSONException { + BufferedReader reader = + new BufferedReader(new InputStreamReader(sock.getInputStream()), 8192); + PrintWriter writer = new PrintWriter(sock.getOutputStream(), true); + String data; + if ((data = reader.readLine()) != null) { + Log.v("Received: " + data); + JSONObject request = new JSONObject(data); + if (request.has("cmd") && request.has("uid")) { + String cmd = request.getString("cmd"); + int uid = request.getInt("uid"); + JSONObject result = new JSONObject(); + if (cmd.equals("initiate")) { + Log.d("Initiate a new session"); + threadIndex += 1; + int mUID = threadIndex; + ConnectionThread networkThread = + new ConnectionThread(sock, true, mUID, reader, writer); + mConnectionThreads.put(mUID, networkThread); + networkThread.start(); + notifyOnConnect(); + result.put("uid", mUID); + result.put("status", true); + result.put("error", null); + } else if (cmd.equals("continue")) { + Log.d("Continue an existing session"); + Log.d("keys: " + mConnectionThreads.keySet().toString()); + if (!mConnectionThreads.containsKey(uid)) { + result.put("uid", uid); + result.put("status", false); + result.put("error", "Session does not exist."); + } else { + ConnectionThread networkThread = + new ConnectionThread(sock, true, uid, reader, writer); + mConnectionThreads.put(uid, networkThread); + networkThread.start(); + notifyOnConnect(); + result.put("uid", uid); + result.put("status", true); + result.put("error", null); + } + } else { + result.put("uid", uid); + result.put("status", false); + result.put("error", "Unrecognized command."); + } + writer.write(result + "\n"); + writer.flush(); + Log.v("Sent: " + result); + } else { + ConnectionThread networkThread = + new ConnectionThread(sock, false, 0, reader, writer); + mConnectionThreads.put(0, networkThread); + networkThread.start(); + notifyOnConnect(); + } + } + } + + public void shutdown() throws Exception { + // Stop listening on the server socket to ensure that + // beyond this point there are no incoming requests. + mStopServer = true; + try { + mServer.close(); + } catch (IOException e) { + Log.e("Failed to close server socket.", e); + } + // Since the server is not running, the mNetworkThreads set can only + // shrink from this point onward. We can just stop all of the running helper + // threads. In the worst case, one of the running threads will already have + // shut down. Since this is a CopyOnWriteList, we don't have to worry about + // concurrency issues while iterating over the set of threads. + for (ConnectionThread connectionThread : mConnectionThreads.values()) { + connectionThread.close(); + } + for (SimpleServerObserver observer : mObservers) { + removeObserver(observer); + } + } +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/schedulerpc/ScheduleRpcSnippet.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/schedulerpc/ScheduleRpcSnippet.java new file mode 100644 index 0000000..2042b7f --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/schedulerpc/ScheduleRpcSnippet.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2017 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.schedulerpc; + +import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.rpc.AsyncRpc; +import com.google.android.mobly.snippet.util.RpcUtil; +import org.json.JSONArray; + +/** Snippet that provides {@link AsyncRpc} to schedule other RPCs. */ +public class ScheduleRpcSnippet implements Snippet { + + private final RpcUtil mRpcUtil; + + public ScheduleRpcSnippet() { + mRpcUtil = new RpcUtil(); + } + + @AsyncRpc(description = "Delay the given RPC by provided milli-seconds.") + public void scheduleRpc( + String callbackId, String methodName, long delayTimerMs, JSONArray params) + throws Throwable { + mRpcUtil.scheduleRpc(callbackId, methodName, delayTimerMs, params); + } + + @Override + public void shutdown() {} +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/AndroidUtil.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/AndroidUtil.java new file mode 100644 index 0000000..46c4940 --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/AndroidUtil.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.util; + +import android.content.Intent; +import android.os.Bundle; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public final class AndroidUtil { + private AndroidUtil() {} + + // TODO(damonkohler): Pull this out into proper argument deserialization and support + // complex/nested types being passed in. + public static void putExtrasFromJsonObject(JSONObject extras, Intent intent) + throws JSONException { + JSONArray names = extras.names(); + for (int i = 0; i < names.length(); i++) { + String name = names.getString(i); + Object data = extras.get(name); + if (data == null) { + continue; + } + if (data instanceof Integer) { + intent.putExtra(name, (Integer) data); + } + if (data instanceof Float) { + intent.putExtra(name, (Float) data); + } + if (data instanceof Double) { + intent.putExtra(name, (Double) data); + } + if (data instanceof Long) { + intent.putExtra(name, (Long) data); + } + if (data instanceof String) { + intent.putExtra(name, (String) data); + } + if (data instanceof Boolean) { + intent.putExtra(name, (Boolean) data); + } + // Nested JSONObject + if (data instanceof JSONObject) { + Bundle nestedBundle = new Bundle(); + intent.putExtra(name, nestedBundle); + putNestedJSONObject((JSONObject) data, nestedBundle); + } + // Nested JSONArray. Doesn't support mixed types in single array + if (data instanceof JSONArray) { + // Empty array. No way to tell what type of data to pass on, so skipping + if (((JSONArray) data).length() == 0) { + Log.e("Empty array not supported in JSONObject, skipping"); + continue; + } + // Integer + if (((JSONArray) data).get(0) instanceof Integer) { + Integer[] integerArrayData = new Integer[((JSONArray) data).length()]; + for (int j = 0; j < ((JSONArray) data).length(); ++j) { + integerArrayData[j] = ((JSONArray) data).getInt(j); + } + intent.putExtra(name, integerArrayData); + } + // Double + if (((JSONArray) data).get(0) instanceof Double) { + Double[] doubleArrayData = new Double[((JSONArray) data).length()]; + for (int j = 0; j < ((JSONArray) data).length(); ++j) { + doubleArrayData[j] = ((JSONArray) data).getDouble(j); + } + intent.putExtra(name, doubleArrayData); + } + // Long + if (((JSONArray) data).get(0) instanceof Long) { + Long[] longArrayData = new Long[((JSONArray) data).length()]; + for (int j = 0; j < ((JSONArray) data).length(); ++j) { + longArrayData[j] = ((JSONArray) data).getLong(j); + } + intent.putExtra(name, longArrayData); + } + // String + if (((JSONArray) data).get(0) instanceof String) { + String[] stringArrayData = new String[((JSONArray) data).length()]; + for (int j = 0; j < ((JSONArray) data).length(); ++j) { + stringArrayData[j] = ((JSONArray) data).getString(j); + } + intent.putExtra(name, stringArrayData); + } + // Boolean + if (((JSONArray) data).get(0) instanceof Boolean) { + Boolean[] booleanArrayData = new Boolean[((JSONArray) data).length()]; + for (int j = 0; j < ((JSONArray) data).length(); ++j) { + booleanArrayData[j] = ((JSONArray) data).getBoolean(j); + } + intent.putExtra(name, booleanArrayData); + } + } + } + } + + // Contributed by Emmanuel T + // Nested Array handling contributed by Sergey Zelenev + private static void putNestedJSONObject(JSONObject jsonObject, Bundle bundle) + throws JSONException { + JSONArray names = jsonObject.names(); + for (int i = 0; i < names.length(); i++) { + String name = names.getString(i); + Object data = jsonObject.get(name); + if (data == null) { + continue; + } + if (data instanceof Integer) { + bundle.putInt(name, ((Integer) data).intValue()); + } + if (data instanceof Float) { + bundle.putFloat(name, ((Float) data).floatValue()); + } + if (data instanceof Double) { + bundle.putDouble(name, ((Double) data).doubleValue()); + } + if (data instanceof Long) { + bundle.putLong(name, ((Long) data).longValue()); + } + if (data instanceof String) { + bundle.putString(name, (String) data); + } + if (data instanceof Boolean) { + bundle.putBoolean(name, ((Boolean) data).booleanValue()); + } + // Nested JSONObject + if (data instanceof JSONObject) { + Bundle nestedBundle = new Bundle(); + bundle.putBundle(name, nestedBundle); + putNestedJSONObject((JSONObject) data, nestedBundle); + } + // Nested JSONArray. Doesn't support mixed types in single array + if (data instanceof JSONArray) { + // Empty array. No way to tell what type of data to pass on, so skipping + if (((JSONArray) data).length() == 0) { + Log.e("Empty array not supported in nested JSONObject, skipping"); + continue; + } + // Integer + if (((JSONArray) data).get(0) instanceof Integer) { + int[] integerArrayData = new int[((JSONArray) data).length()]; + for (int j = 0; j < ((JSONArray) data).length(); ++j) { + integerArrayData[j] = ((JSONArray) data).getInt(j); + } + bundle.putIntArray(name, integerArrayData); + } + // Double + if (((JSONArray) data).get(0) instanceof Double) { + double[] doubleArrayData = new double[((JSONArray) data).length()]; + for (int j = 0; j < ((JSONArray) data).length(); ++j) { + doubleArrayData[j] = ((JSONArray) data).getDouble(j); + } + bundle.putDoubleArray(name, doubleArrayData); + } + // Long + if (((JSONArray) data).get(0) instanceof Long) { + long[] longArrayData = new long[((JSONArray) data).length()]; + for (int j = 0; j < ((JSONArray) data).length(); ++j) { + longArrayData[j] = ((JSONArray) data).getLong(j); + } + bundle.putLongArray(name, longArrayData); + } + // String + if (((JSONArray) data).get(0) instanceof String) { + String[] stringArrayData = new String[((JSONArray) data).length()]; + for (int j = 0; j < ((JSONArray) data).length(); ++j) { + stringArrayData[j] = ((JSONArray) data).getString(j); + } + bundle.putStringArray(name, stringArrayData); + } + // Boolean + if (((JSONArray) data).get(0) instanceof Boolean) { + boolean[] booleanArrayData = new boolean[((JSONArray) data).length()]; + for (int j = 0; j < ((JSONArray) data).length(); ++j) { + booleanArrayData[j] = ((JSONArray) data).getBoolean(j); + } + bundle.putBooleanArray(name, booleanArrayData); + } + } + } + } +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/EmptyTestClass.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/EmptyTestClass.java new file mode 100644 index 0000000..ac1920f --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/EmptyTestClass.java @@ -0,0 +1,28 @@ +/* + * 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.util; + +import org.junit.Ignore; + +/** + * A stub JUnit class with no tests. + * + * <p>Used for 'safely' calling AndroidJUnitRunner methods on snippets that happen to have tests + * defined, to avoid actually calling those tests. + */ +@Ignore +public class EmptyTestClass {} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/Log.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/Log.java new file mode 100644 index 0000000..64f03e9 --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/Log.java @@ -0,0 +1,146 @@ +/* + * 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.util; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Bundle; + +public final class Log { + public static volatile String apkLogTag = null; + + private static final String MY_CLASS_NAME = Log.class.getName(); + private static final String ANDROID_LOG_CLASS_NAME = android.util.Log.class.getName(); + + // Skip the first two entries in stack trace when trying to infer the caller. + // The first two entries are: + // - dalvik.system.VMStack.getThreadStackTrace(Native Method) + // - java.lang.Thread.getStackTrace(Thread.java:580) + // The {@code getStackTrace()} function returns the stack trace at where the trace is collected + // (inisde the JNI function {@code getThreadStackTrace()} instead of at where the {@code + // getStackTrace()} is called (althrought this is the natual expectation). + private static final int STACK_TRACE_WALK_START_INDEX = 2; + + private Log() {} + + public static synchronized void initLogTag(Context context) { + if (apkLogTag != null) { + throw new IllegalStateException("Logger should not be re-initialized"); + } + String packageName = context.getPackageName(); + PackageManager packageManager = context.getPackageManager(); + ApplicationInfo appInfo; + try { + appInfo = packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA); + } catch (NameNotFoundException e) { + throw new IllegalStateException( + "Failed to find ApplicationInfo with package name: " + packageName); + } + Bundle bundle = appInfo.metaData; + apkLogTag = bundle.getString("mobly-log-tag"); + if (apkLogTag == null) { + apkLogTag = packageName; + w( + "AndroidManifest.xml does not contain metadata field named \"mobly-log-tag\". " + + "Using package name for logging instead."); + } + } + + private static String getTag() { + String logTag = apkLogTag; + if (logTag == null) { + throw new IllegalStateException("Logging called before initLogTag()"); + } + StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); + + boolean isCallerClassNameFound = false; + String fullClassName = null; + int lineNumber = 0; + // Walk up the stack and look for the first class name that is neither us nor + // android.util.Log: that's the caller. + // Do not used hard-coded stack depth: that does not work all the time because of proguard + // inline optimization. + for (int i = STACK_TRACE_WALK_START_INDEX; i < stackTraceElements.length; i++) { + StackTraceElement element = stackTraceElements[i]; + fullClassName = element.getClassName(); + if (!fullClassName.equals(MY_CLASS_NAME) + && !fullClassName.equals(ANDROID_LOG_CLASS_NAME)) { + lineNumber = element.getLineNumber(); + isCallerClassNameFound = true; + break; + } + } + + if (!isCallerClassNameFound) { + // Failed to determine caller's class name, fall back the the minimal one. + return logTag; + } else { + String className = fullClassName.substring(fullClassName.lastIndexOf(".") + 1); + return logTag + "." + className + ":" + lineNumber; + } + } + + public static void v(String message) { + android.util.Log.v(getTag(), message); + } + + public static void v(String message, Throwable e) { + android.util.Log.v(getTag(), message, e); + } + + public static void e(Throwable e) { + android.util.Log.e(getTag(), "Error", e); + } + + public static void e(String message) { + android.util.Log.e(getTag(), message); + } + + public static void e(String message, Throwable e) { + android.util.Log.e(getTag(), message, e); + } + + public static void w(Throwable e) { + android.util.Log.w(getTag(), "Warning", e); + } + + public static void w(String message) { + android.util.Log.w(getTag(), message); + } + + public static void w(String message, Throwable e) { + android.util.Log.w(getTag(), message, e); + } + + public static void d(String message) { + android.util.Log.d(getTag(), message); + } + + public static void d(String message, Throwable e) { + android.util.Log.d(getTag(), message, e); + } + + public static void i(String message) { + android.util.Log.i(getTag(), message); + } + + public static void i(String message, Throwable e) { + android.util.Log.i(getTag(), message, e); + } +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/MainThread.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/MainThread.java new file mode 100644 index 0000000..0e4ece5 --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/MainThread.java @@ -0,0 +1,90 @@ +/* + * 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.util; + +import android.os.Handler; +import android.os.Looper; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; + +public class MainThread { + /** + * Wraps a {@link Callable} in a {@link Runnable} that has a way to get the return value and + * exception after the fact. + */ + private static class CallableWrapper<T> implements Runnable { + private final Callable<T> mCallable; + private final CountDownLatch mLatch = new CountDownLatch(1); + private T mReturnValue; + private Throwable mException; + + public CallableWrapper(Callable<T> callable) { + mCallable = callable; + } + + @Override + public final void run() { + try { + mReturnValue = mCallable.call(); + } catch (Throwable t) { + mException = t; + } finally { + mLatch.countDown(); + } + } + + public void awaitTermination() throws InterruptedException { + mLatch.await(); + } + + public T getReturnValue() { + return mReturnValue; + } + + public Throwable getException() { + return mException; + } + } + + private static final Handler sMainThreadHandler = new Handler(Looper.getMainLooper()); + + private MainThread() { + // Utility class. + } + + /** Executed in the main thread. Returns the result of an execution or any exception thrown. */ + public static <T> T run(final Callable<T> task) throws Exception { + CallableWrapper<T> wrapper = new CallableWrapper<>(task); + return runCallableWrapper(wrapper); + } + + private static <T> T runCallableWrapper(CallableWrapper<T> wrapper) throws Exception { + sMainThreadHandler.post(wrapper); + wrapper.awaitTermination(); + Throwable exception = wrapper.getException(); + if (exception != null) { + if (exception instanceof RuntimeException) { + throw (RuntimeException) exception; + } + if (exception instanceof Error) { + throw (Error) exception; + } + throw (Exception) exception; + } + return wrapper.getReturnValue(); + } +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/NotificationIdFactory.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/NotificationIdFactory.java new file mode 100644 index 0000000..39cea5e --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/NotificationIdFactory.java @@ -0,0 +1,33 @@ +/* + * 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.util; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Creates unique ids to identify the notifications created by the android scripting service and the + * trigger service. + */ +public final class NotificationIdFactory { + private static final AtomicInteger mNextId = new AtomicInteger(0); + + public static int create() { + return mNextId.incrementAndGet(); + } + + private NotificationIdFactory() {} +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/RpcUtil.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/RpcUtil.java new file mode 100644 index 0000000..4601e93 --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/RpcUtil.java @@ -0,0 +1,139 @@ +/* + * 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.util; + +import com.google.android.mobly.snippet.event.EventCache; +import com.google.android.mobly.snippet.event.SnippetEvent; +import com.google.android.mobly.snippet.manager.SnippetManager; +import com.google.android.mobly.snippet.rpc.JsonRpcResult; +import com.google.android.mobly.snippet.rpc.MethodDescriptor; +import com.google.android.mobly.snippet.rpc.RpcError; +import java.util.Locale; +import java.util.Timer; +import java.util.TimerTask; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Class that implements APIs to schedule other RPCs. + * + * <p>If a device is required to be disconnected (e.g., USB power off), no RPCs can be made while + * device is offline. + * + * <p>However, We still need snippet continue to run and execute previously scheduled RPCs + * + * <p>The return value of the scheduled RPC is cached in {@link EventCache} and can be retrieved + * later after device is back online. + */ +public class RpcUtil { + // RPC ID is used for reporting responses back to the client. However, the results of + // scheduled RPCs are reported back to the client via events instead of through synchronous + // responses, so the RPC ID is unused. We pass an arbitrary value of 0. + private static final int DEFAULT_ID = 0; + private final SnippetManager mReceiverManager; + private final EventCache mEventCache = EventCache.getInstance(); + + public RpcUtil() { + mReceiverManager = SnippetManager.getInstance(); + } + + /** + * Schedule given RPC with some delay. + * + * @param callbackId The callback ID used to cache RPC results. + * @param methodName The RPC name to be scheduled. + * @param delayMs The delay in ms + * @param params Array of the parameters to the RPC + */ + public void scheduleRpc( + final String callbackId, + final String methodName, + final long delayMs, + final JSONArray params) + throws Throwable { + Timer timer = new Timer(); + TimerTask task = + new TimerTask() { + @Override + public void run() { + SnippetEvent event = new SnippetEvent(callbackId, methodName); + try { + JSONObject obj = invokeRpc(methodName, params, DEFAULT_ID, callbackId); + // Cache RPC method return value. + for (int i = 0; i < obj.names().length(); i++) { + String key = obj.names().getString(i); + event.getData().putString(key, obj.get(key).toString()); + } + } catch (JSONException e) { + String stackTrace = JsonRpcResult.getStackTrace(e); + event.getData().putString("error", stackTrace); + } finally { + mEventCache.postEvent(event); + } + } + }; + timer.schedule(task, delayMs); + } + + /** + * Invoke the RPC. + * + * @param methodName The RPC name to be invoked. + * @param params Array of the parameters to the RPC + * @param id The ID that identifies an RPC + * @param UID Globally unique session ID. + */ + public JSONObject invokeRpc(String methodName, JSONArray params, int id, Integer UID) + throws JSONException { + return invokeRpc(methodName, params, id, String.format(Locale.US, "%d-%d", UID, id)); + } + + /** + * Invoke the RPC. + * + * @param methodName The RPC name to be invoked. + * @param params Array of the parameters to the RPC + * @param id The ID that identifies an RPC + * @param callbackId The callback ID used to cache RPC results. + */ + public JSONObject invokeRpc(String methodName, JSONArray params, int id, String callbackId) + throws JSONException { + MethodDescriptor rpc = mReceiverManager.getMethodDescriptor(methodName); + if (rpc == null) { + return JsonRpcResult.error(id, new RpcError("Unknown RPC: " + methodName)); + } + try { + JSONArray newParams = new JSONArray(); + /** If calling an {@link AsyncRpc}, put the message ID as the first param. */ + if (rpc.isAsync()) { + newParams.put(callbackId); + for (int i = 0; i < params.length(); i++) { + newParams.put(params.get(i)); + } + Object returnValue = rpc.invoke(mReceiverManager, newParams); + return JsonRpcResult.callback(id, returnValue, callbackId); + } else { + Object returnValue = rpc.invoke(mReceiverManager, params); + return JsonRpcResult.result(id, returnValue); + } + } catch (Throwable t) { + Log.e("Invocation error.", t); + return JsonRpcResult.error(id, t); + } + } +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/SnippetLibException.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/SnippetLibException.java new file mode 100644 index 0000000..e6f5af7 --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/SnippetLibException.java @@ -0,0 +1,33 @@ +/* + * 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.util; + +@SuppressWarnings("serial") +public class SnippetLibException extends Exception { + + public SnippetLibException(Exception e) { + super(e); + } + + public SnippetLibException(String message) { + super(message); + } + + public SnippetLibException(String message, Exception e) { + super(message, e); + } +} |