Commit a2da02bd authored by serya's avatar serya Committed by Commit bot

Registration of DevTools bridge in GCD.

Code for registering and unregistering instance of DevTools bridge in GCD.
GCDRegistrationFragment is able to perform registration including:
1. Asking user for desired account to register.
2. Requesting OAuth permissions for GCD (may show confirmation activity to the user).
3. Registration in Cloud Messages if needed.

It also shows toasts with registration/unregistration results.

GCDRegistrationFragment is absstract and has no views. Testing implementation
shows button "Registed in GCD/Unregister (registered for ...)". Registration is not
persistent yet.

BUG=383418
TEST=New button in main activity of DevToolsBridgeTest.apk: register/unregister.

Review URL: https://codereview.chromium.org/677843006

Cr-Commit-Position: refs/heads/master@{#302251}
parent 8851b7f0
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
}, },
'includes': [ '../build/java.gypi' ], 'includes': [ '../build/java.gypi' ],
'dependencies': [ 'dependencies': [
'../third_party/android_tools/android_tools.gyp:android_gcm',
'../third_party/libjingle/libjingle.gyp:libjingle_peerconnection_javalib', '../third_party/libjingle/libjingle.gyp:libjingle_peerconnection_javalib',
], ],
}, },
......
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.devtools_bridge.apiary;
import android.net.http.AndroidHttpClient;
/**
* Factory for creating clients for external APIs.
*/
public abstract class ApiaryClientFactory {
private static final String USER_AGENT = "DevTools bridge";
public static final String OAUTH_SCOPE = "https://www.googleapis.com/auth/clouddevices";
private final AndroidHttpClient mHttpClient = AndroidHttpClient.newInstance(USER_AGENT);
/**
* Creates a new GCD client with auth token.
*/
public GCDClient newGCDClient(String oAuthToken) {
return new GCDClient(mHttpClient, getAPIKey(), oAuthToken);
}
/**
* Creates a new anonymous client. GCD requires client been not authenticated by user or
* device credentials for finalizing registration.
*/
public GCDClient newAnonymousGCDClient() {
return new GCDClient(mHttpClient, getAPIKey());
}
public OAuthClient newOAuthClient() {
return new OAuthClient(
mHttpClient, OAUTH_SCOPE, getOAuthClientId(), getOAuthClientSecret());
}
public BlockingGCMRegistrar newGCMRegistrar() {
return new BlockingGCMRegistrar() {
@Override
protected String[] getSenderIds() {
return getGCMSenderIds();
}
};
}
public void close() {
mHttpClient.close();
}
public abstract String getAPIKey();
public abstract String getOAuthClientId();
public abstract String getOAuthClientSecret();
public abstract String[] getGCMSenderIds();
}
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.devtools_bridge.apiary;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.util.Log;
import com.google.android.gcm.GCMConstants;
import com.google.android.gcm.GCMRegistrar;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
/**
* Helps using GCDRegistrar in blocking manner. If the app has not registered in GCM
* it sends registration request and waits for an intent with registration ID.
* Waiting may be interrupted. Must not be used on UI (or Context's main) looper.
*/
public abstract class BlockingGCMRegistrar {
private static final String TAG = "BlockingGCMRegistrar";
public String blockingGetRegistrationId(Context context)
throws InterruptedException, IOException {
assert context != null;
Receiver receiver = new Receiver();
receiver.register(context);
try {
String result = GCMRegistrar.getRegistrationId(context);
if (result != null && !result.isEmpty()) return result;
GCMRegistrar.register(context, getSenderIds());
return receiver.awaitRegistrationId();
} finally {
receiver.unregister(context);
}
}
protected abstract String[] getSenderIds();
private static class Receiver extends BroadcastReceiver {
private final CountDownLatch mDone = new CountDownLatch(1);
private String mRegistrationId;
private String mError;
public void register(Context context) {
IntentFilter filter = new IntentFilter();
filter.addCategory(context.getPackageName());
filter.addAction(GCMConstants.INTENT_FROM_GCM_REGISTRATION_CALLBACK);
context.registerReceiver(this, filter);
}
public void unregister(Context context) {
context.unregisterReceiver(this);
}
public String awaitRegistrationId() throws InterruptedException, IOException {
mDone.await();
if (mRegistrationId != null) {
return mRegistrationId;
}
throw new IOException(mError);
}
@Override
public void onReceive(Context context, Intent intent) {
assert intent.getAction().equals(GCMConstants.INTENT_FROM_GCM_REGISTRATION_CALLBACK);
mRegistrationId = intent.getStringExtra(GCMConstants.EXTRA_REGISTRATION_ID);
mError = intent.getStringExtra(GCMConstants.EXTRA_ERROR);
if (mRegistrationId != null || mError != null) {
mDone.countDown();
} else {
Log.e(TAG, "Unexpected intent: " + intent);
}
}
}
}
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.devtools_bridge.apiary;
import android.util.JsonReader;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.StringEntity;
import org.chromium.components.devtools_bridge.gcd.InstanceCredential;
import org.chromium.components.devtools_bridge.gcd.InstanceDescription;
import org.chromium.components.devtools_bridge.gcd.MessageReader;
import org.chromium.components.devtools_bridge.gcd.MessageWriter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
/**
* Client for accessing GCD API.
*/
public class GCDClient {
private static final String API_BASE = "https://www.googleapis.com/clouddevices/v1";
public static final String ENCODING = "UTF-8";
protected static final String CONTENT_TYPE = "application/json; charset=" + ENCODING;
private final HttpClient mHttpClient;
private final String mAPIKey;
private final String mOAuthToken;
GCDClient(HttpClient httpClient, String apiKey, String oAuthToken) {
mHttpClient = httpClient;
mAPIKey = apiKey;
mOAuthToken = oAuthToken;
}
GCDClient(HttpClient httpClient, String apiKey) {
this(httpClient, apiKey, null);
}
/**
* Creation of a registration ticket is the first step in instance registration. Client must
* have user credentials. If the ticket has been registered it will be associated with the
* user. Next step is registration ticket patching.
*/
public String createRegistrationTicket() throws IOException {
assert mOAuthToken != null;
return mHttpClient.execute(
newHttpPost("/registrationTickets", "{\"userEmail\":\"me\"}"),
new JsonResponseHandler<String>() {
@Override
public String readResponse(JsonReader reader) throws IOException {
return new MessageReader(reader).readTicketId();
}
});
}
/**
* Patching registration ticket. GCD gets device definition including commands metadata,
* GCM channel description and user-visible instance name.
*/
public void patchRegistrationTicket(String ticketId, InstanceDescription description)
throws IOException {
String content = new MessageWriter().writeTicketPatch(description).close().toString();
mHttpClient.execute(
newHttpPatch("/registrationTickets/" + ticketId, content),
new EmptyResponseHandler());
}
/**
* Finalizing registration. Client must be anonymous (GCD requirement). GCD provides
* instance credentials needed for handling commands.
*/
public InstanceCredential finalizeRegistration(String ticketId) throws IOException {
return mHttpClient.execute(
newHttpPost("/registrationTickets/" + ticketId + "/finalize", ""),
new JsonResponseHandler<InstanceCredential>() {
@Override
public InstanceCredential readResponse(JsonReader reader) throws IOException {
return new MessageReader(reader).readInstanceCredential();
}
});
}
/**
* Deletes registered instance (unregisters). If client has instance credentials then
* instanceId must be it's own ID. If client has user credentials then instance must belong
* to the user.
*/
public void deleteInstance(String instanceId) throws IOException {
mHttpClient.execute(
newHttpDelete("/devices/" + instanceId),
new EmptyResponseHandler());
}
private HttpPost newHttpPost(String path, String content) throws UnsupportedEncodingException {
HttpPost request = new HttpPost(buildUrl(path));
setContent(request, content);
initializeRequest(request);
return request;
}
private HttpPatch newHttpPatch(String path, String content)
throws UnsupportedEncodingException {
HttpPatch request = new HttpPatch(buildUrl(path));
setContent(request, content);
initializeRequest(request);
return request;
}
private HttpDelete newHttpDelete(String path) {
HttpDelete request = new HttpDelete(buildUrl(path));
initializeRequest(request);
return request;
}
private String buildUrl(String path) {
return API_BASE + path + "?key=" + mAPIKey;
}
private void setContent(HttpEntityEnclosingRequestBase request, String content)
throws UnsupportedEncodingException {
request.setEntity(new StringEntity(content, ENCODING));
request.addHeader("Content-Type", CONTENT_TYPE);
}
private void initializeRequest(HttpRequestBase request) {
if (mOAuthToken != null) {
request.addHeader("Authorization", "Bearer " + mOAuthToken);
}
}
private static final class HttpPatch extends HttpEntityEnclosingRequestBase {
public HttpPatch(String uri) {
setURI(URI.create(uri));
}
public String getMethod() {
return "PATCH";
}
}
private static class EmptyResponseHandler implements ResponseHandler<Void> {
@Override
public Void handleResponse(HttpResponse response) throws HttpResponseException {
JsonResponseHandler.checkStatus(response);
return null;
}
}
}
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.devtools_bridge.apiary;
import android.util.JsonReader;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.ResponseHandler;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* Base class for a ResponseHandler that reads response with JsonReader. Like BasicResponseHandler
* throws HttpResponseException if response code >= 300.
*
* It catchs JsonReader's runtime exception (IllegalStateException and IllegalArgumentException)
* and wraps them into ResponseFormatException.
*/
abstract class JsonResponseHandler<T> implements ResponseHandler<T> {
public static void checkStatus(HttpResponse response) throws HttpResponseException {
StatusLine statusLine = response.getStatusLine();
if (response.getStatusLine().getStatusCode() >= 300) {
throw new HttpResponseException(
statusLine.getStatusCode(), statusLine.getReasonPhrase());
}
}
@Override
public final T handleResponse(HttpResponse response)
throws IOException, ClientProtocolException {
checkStatus(response);
HttpEntity entity = response.getEntity();
if (entity == null) {
throw new ClientProtocolException("Missing content");
}
JsonReader reader = new JsonReader(new InputStreamReader(entity.getContent()));
try {
T result = readResponse(reader);
reader.close();
if (result == null) {
throw new ClientProtocolException("Missing result");
}
return result;
} catch (IllegalStateException e) {
throw new ResponseFormatException(e);
} catch (IllegalArgumentException e) {
throw new ResponseFormatException(e);
}
}
public abstract T readResponse(JsonReader reader)
throws IOException, ResponseFormatException;
public static class ResponseFormatException extends ClientProtocolException {
public ResponseFormatException(RuntimeException readerException) {
super(readerException);
}
public ResponseFormatException() {}
}
}
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.devtools_bridge.apiary;
import android.util.JsonReader;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
/**
* Google authentication client. Fetches a pair of refresh/access tokens for a
* secret received while registering the instance in GCD.
*/
public class OAuthClient {
public static final String API_BASE = "https://accounts.google.com/o/oauth2";
public static final String ENCODING = "UTF-8";
public static final String CONTENT_TYPE = "application/x-www-form-urlencoded";
private final HttpClient mHttpClient;
private final String mScope;
private final String mClientId;
private final String mClientSecret;
OAuthClient(HttpClient httpClient, String scope, String clientId, String clientSecret) {
assert httpClient != null;
assert scope != null;
assert clientId != null;
assert clientSecret != null;
mHttpClient = httpClient;
mScope = scope;
mClientId = clientId;
mClientSecret = clientSecret;
}
public OAuthResult authenticate(String secret) throws IOException {
final long startTimeMs = System.currentTimeMillis();
String content =
"client_id=" + urlEncode(mClientId)
+ "&client_secret=" + urlEncode(mClientSecret)
+ "&scope=" + urlEncode(mScope)
+ "&code=" + urlEncode(secret)
+ "&redirect_uri=oob"
+ "&grant_type=authorization_code";
return mHttpClient.execute(
newHttpPost("/token", content),
new JsonResponseHandler<OAuthResult>() {
@Override
public OAuthResult readResponse(JsonReader reader) throws IOException {
return readResponse(reader, startTimeMs);
}
});
}
private HttpPost newHttpPost(String path, String content) throws UnsupportedEncodingException {
HttpPost request = new HttpPost(API_BASE + "/token");
request.setEntity(new StringEntity(content, ENCODING));
request.addHeader("Content-Type", CONTENT_TYPE);
return request;
}
private static String urlEncode(String value) {
try {
return URLEncoder.encode(value, ENCODING);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
static OAuthResult readResponse(JsonReader reader, long startTimeMs) throws IOException {
String refreshToken = null;
String accessToken = null;
long expiresInS = 0; // In seconds.
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
if (name.equals("refresh_token")) {
refreshToken = reader.nextString();
} else if (name.equals("access_token")) {
accessToken = reader.nextString();
} else if (name.equals("expires_in")) {
expiresInS = reader.nextLong();
} else {
reader.skipValue();
}
}
reader.endObject();
return OAuthResult.create(
refreshToken, accessToken, startTimeMs + expiresInS * 1000);
}
}
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.devtools_bridge.apiary;
/**
* Pair of refresh/access tokens fetched with OAuthClient.
*/
public class OAuthResult {
public String refreshToken;
public String accessToken;
public final long expirationTimeMs; // In milliseconds.
private OAuthResult(String refreshToken, String accessToken, long expirationTimeMs) {
assert refreshToken != null;
this.refreshToken = refreshToken;
this.accessToken = accessToken;
this.expirationTimeMs = expirationTimeMs;
}
public static OAuthResult create(
String refreshToken, String accessToken, long expirationTimeMs) {
return refreshToken != null
? new OAuthResult(refreshToken, accessToken, expirationTimeMs) : null;
}
}
...@@ -13,14 +13,14 @@ import java.util.Map; ...@@ -13,14 +13,14 @@ import java.util.Map;
*/ */
public abstract class CommandDefinition { public abstract class CommandDefinition {
private final String mName; private final String mName;
private final List<ParamDefinition> mInParams; private final List<ParamDefinition<?>> mInParams;
public CommandDefinition(String name, List<ParamDefinition> inParams) { public CommandDefinition(String name, List<ParamDefinition<?>> inParams) {
mName = name; mName = name;
mInParams = inParams; mInParams = inParams;
} }
public Iterable<ParamDefinition> inParams() { public Iterable<ParamDefinition<?>> inParams() {
return mInParams; return mInParams;
} }
......
...@@ -111,7 +111,7 @@ final class Commands { ...@@ -111,7 +111,7 @@ final class Commands {
} }
@Override @Override
protected ParamDefinition resultDefinition() { protected ParamDefinition<String> resultDefinition() {
return PARAM_ANSWER; return PARAM_ANSWER;
} }
} }
...@@ -149,7 +149,7 @@ final class Commands { ...@@ -149,7 +149,7 @@ final class Commands {
} }
@Override @Override
protected ParamDefinition resultDefinition() { protected ParamDefinition<List<String>> resultDefinition() {
return PARAM_SERVER_CANDIDATES; return PARAM_SERVER_CANDIDATES;
} }
} }
...@@ -185,12 +185,12 @@ final class Commands { ...@@ -185,12 +185,12 @@ final class Commands {
} }
@Override @Override
protected ParamDefinition resultDefinition() { protected ParamDefinition<String> resultDefinition() {
return PARAM_ANSWER; return PARAM_ANSWER;
} }
} }
private static List<ParamDefinition> params(ParamDefinition... values) { private static List<ParamDefinition<?>> params(ParamDefinition<?>... values) {
return Collections.unmodifiableList(Arrays.asList(values)); return Collections.unmodifiableList(Arrays.asList(values));
} }
} }
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.devtools_bridge.gcd;
/**
* Information provided by GCD when instance has registered. Instance id
* can be used for:
* 1. Making sure incoming messages are addressed to the instance.
* 2. It needed when sending command results to GCD.
* 3. For device unregistration.
*
* The Secret supposed to be used to authenticate the instance with OAuthClient
* (it has no user credentials).
*/
public final class InstanceCredential {
public final String id;
public final String secret;
public InstanceCredential(String id, String secret) {
assert id != null;
assert secret != null;
this.id = id;
this.secret = secret;
}
}
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.devtools_bridge.gcd;
/**
* Information needed for registration in GCD.
* Instance secret will be bound to oAuthClientId.
* gcmChannelId will be used for delivering commands.
* displayName is a human readable name on the client side.
*/
public final class InstanceDescription {
public final String oAuthClientId;
public final String gcmChannelId;
public final String displayName;
private InstanceDescription(String oAuthClientId, String gcmChannelId, String displayName) {
assert oAuthClientId != null;
assert gcmChannelId != null;
assert displayName != null;
this.oAuthClientId = oAuthClientId;
this.gcmChannelId = gcmChannelId;
this.displayName = displayName;
}
/**
* Builder for InstanceDescription.
*/
public static final class Builder {
private String mOAuthClientId;
private String mGCMChannelId;
private String mDisplayName;
public Builder setOAuthClientId(String value) {
mOAuthClientId = value;
return this;
}
public Builder setGCMChannelId(String value) {
mGCMChannelId = value;
return this;
}
public Builder setDisplayName(String value) {
mDisplayName = value;
return this;
}
public InstanceDescription build() {
return new InstanceDescription(mOAuthClientId, mGCMChannelId, mDisplayName);
}
}
}
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.devtools_bridge.gcd;
import android.util.JsonReader;
import java.io.IOException;
/**
* Helper class for parsing JSON-encoded GCD messages (HTTP responses and GCM notifications) used
* in the DevTools bridge.
*/
public final class MessageReader {
private final JsonReader mReader;
public MessageReader(JsonReader reader) {
mReader = reader;
}
/**
* Reads id from a registration ticket.
*/
public String readTicketId() throws IOException {
return new TicketReader().readId();
}
/**
* Reads credentials from finalized registration ticket.
*/
public InstanceCredential readInstanceCredential() throws IOException {
return new TicketReader().readCredential();
}
private abstract class ObjectReader {
public final void readObject() throws IOException {
mReader.beginObject();
while (mReader.hasNext()) {
readItem(mReader.nextName());
}
mReader.endObject();
}
protected void readItem(String name) throws IOException {
mReader.skipValue();
}
}
private class TicketReader extends ObjectReader {
private String mId;
private String mDeviceId;
private String mDeviceSecret;
public String readId() throws IOException {
readObject();
if (mId == null) {
throw new IllegalArgumentException();
}
return mId;
}
public InstanceCredential readCredential() throws IOException {
readObject();
if (mDeviceId == null || mDeviceSecret == null) {
throw new IllegalArgumentException();
}
return new InstanceCredential(mDeviceId, mDeviceSecret);
}
@Override
protected void readItem(String name) throws IOException {
if (name.equals("id")) {
mId = mReader.nextString();
} else if (name.equals("deviceId")) {
mDeviceId = mReader.nextString();
} else if (name.equals("robotAccountAuthorizationCode")) {
mDeviceSecret = mReader.nextString();
} else {
super.readItem(name);
}
}
}
}
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.devtools_bridge.gcd;
import android.util.JsonWriter;
import org.chromium.components.devtools_bridge.commands.Command;
import org.chromium.components.devtools_bridge.commands.ParamDefinition;
import java.io.IOException;
import java.io.StringWriter;
/**
* Helper class for constructing GCD JSON messages (HTTP requests) used in the DevTools bridge.
*/
public final class MessageWriter {
private final StringWriter mStringWriter;
private final JsonWriter mWriter;
boolean mClosed = false;
public MessageWriter() {
mStringWriter = new StringWriter();
mWriter = new JsonWriter(mStringWriter);
}
public MessageWriter close() throws IOException {
assert !mClosed;
mWriter.close();
mClosed = true;
return this;
}
@Override
public String toString() {
assert mClosed;
return mStringWriter.toString();
}
/**
* Write body of registrationTicket PATCH request.
*/
public MessageWriter writeTicketPatch(InstanceDescription description) throws IOException {
mWriter.beginObject();
mWriter.name("deviceDraft");
writeDeviceDraft(description);
mWriter.name("oauthClientId").value(description.oAuthClientId);
mWriter.endObject();
return this;
}
private void writeDeviceDraft(InstanceDescription description) throws IOException {
mWriter.beginObject();
mWriter.name("deviceKind").value("vendor");
mWriter.name("displayName").value(description.displayName);
mWriter.name("systemName").value("Chrome DevTools Bridge");
mWriter.name("channel");
writeChannelDefinition(description);
mWriter.name("commandDefs");
writeCommandsDefinition();
mWriter.endObject();
}
private void writeChannelDefinition(InstanceDescription description) throws IOException {
mWriter.beginObject();
mWriter.name("supportedType").value("gcm");
mWriter.name("gcmRegistrationId").value(description.gcmChannelId);
mWriter.endObject();
}
private void writeCommandsDefinition() throws IOException {
mWriter.beginObject();
mWriter.name("base");
writeCommandsDefinitionBase();
mWriter.endObject();
}
private void writeCommandsDefinitionBase() throws IOException {
mWriter.beginObject();
for (Command.Type type : Command.Type.values()) {
mWriter.name(type.definition.shortName());
beginParameters();
for (ParamDefinition<?> param : type.definition.inParams()) {
writeParameter(param.name(), param.type());
}
endParameters();
}
mWriter.endObject();
}
private void beginParameters() throws IOException {
mWriter.beginObject();
mWriter.name("parameters");
mWriter.beginObject();
}
private void endParameters() throws IOException {
mWriter.endObject();
mWriter.endObject();
}
private void writeParameter(String name, String type) throws IOException {
mWriter.name(name);
mWriter.beginObject();
mWriter.name("type").value(type);
mWriter.endObject();
}
}
...@@ -30,6 +30,8 @@ ...@@ -30,6 +30,8 @@
<uses-permission android:name="android.permission.RUN_INSTRUMENTATION" /> <uses-permission android:name="android.permission.RUN_INSTRUMENTATION" />
<uses-permission android:name="android.permission.INJECT_EVENTS" /> <uses-permission android:name="android.permission.INJECT_EVENTS" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<!-- For manual testing with Chrome Shell --> <!-- For manual testing with Chrome Shell -->
<uses-permission android:name="org.chromium.chrome.shell.permission.DEBUG" /> <uses-permission android:name="org.chromium.chrome.shell.permission.DEBUG" />
......
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.devtools_bridge.apiary;
import android.test.InstrumentationTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.JsonReader;
import junit.framework.Assert;
import org.apache.http.ProtocolVersion;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpResponseException;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicHttpResponse;
import java.io.IOException;
/**
* Tests for {@link JsonResponseHandler}.
*/
public class JsonResponseHandlerTest extends InstrumentationTestCase {
@SmallTest
public void testInvalidResponse() throws IOException {
try {
new JsonResponseHandler<String>() {
@Override
public String readResponse(JsonReader reader) {
return "";
}
}.handleResponse(newResponse(404, null));
Assert.fail();
} catch (HttpResponseException e) {
// Expected
}
}
@SmallTest
public void testIOException() {
try {
new JsonResponseHandler<String>() {
@Override
public String readResponse(JsonReader reader) throws IOException {
throw new IOException();
}
}.handleResponse(newResponse(200, ""));
Assert.fail();
} catch (IOException e) {
// Expected
}
}
@SmallTest
public void testStateException() throws IOException {
try {
new JsonResponseHandler<String>() {
@Override
public String readResponse(JsonReader reader) throws IOException {
reader.beginObject();
reader.endObject();
return "";
}
}.handleResponse(newResponse(200, "[]"));
Assert.fail();
} catch (JsonResponseHandler.ResponseFormatException e) {
// Expected
}
}
@SmallTest
public void testFormatException() throws IOException {
try {
new JsonResponseHandler<String>() {
@Override
public String readResponse(JsonReader reader) throws IOException {
reader.beginArray();
reader.nextLong();
reader.endArray();
return "";
}
}.handleResponse(newResponse(200, "[\"XXX\"]"));
Assert.fail();
} catch (JsonResponseHandler.ResponseFormatException e) {
// Expected
}
}
@SmallTest
public void testNullResultException() throws IOException {
try {
new JsonResponseHandler<String>() {
@Override
public String readResponse(JsonReader reader) throws IOException {
reader.beginArray();
reader.endArray();
return null;
}
}.handleResponse(newResponse(200, "[]"));
Assert.fail();
} catch (ClientProtocolException e) {
// Expected
}
}
@SmallTest
public void testSuccess() throws IOException {
String result = new JsonResponseHandler<String>() {
@Override
public String readResponse(JsonReader reader) throws IOException {
reader.beginArray();
reader.endArray();
return "OK";
}
}.handleResponse(newResponse(200, "[]"));
Assert.assertEquals("OK", result);
}
private BasicHttpResponse newResponse(int status, String content) {
BasicHttpResponse response = new BasicHttpResponse(
new ProtocolVersion("HTTP", 1, 1), status, "reason");
if (content != null) {
try {
response.setEntity(new StringEntity(content, "UTF-8"));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return response;
}
}
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.devtools_bridge.apiary;
import android.test.InstrumentationTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import junit.framework.Assert;
import org.chromium.components.devtools_bridge.util.TestSource;
/**
* Tests for {@link OAuthClient}.
*/
public class OAuthClientTest extends InstrumentationTestCase {
private static final String ACCESS_TOKEN =
"ya29.rgBgy64Y1MACXNmPDUpPGbwFuAec2NCCDJwaEp8DwLnV8RBk45p9RBqBfEQUYxL6OVB-oyktRqZj0w";
private static final String REFRESH_TOKEN =
"1/cWihsJmDMujYfhzBVTwgh4ukiFyiiRWLmFwTv4EigzU";
@SmallTest
public void testResponse() throws Exception {
TestSource source = new TestSource();
source.write()
.beginObject()
.name("access_token").value(ACCESS_TOKEN)
.name("token_type").value("Bearer")
.name("expires_in").value(3600) // seconds
.name("refresh_token").value(REFRESH_TOKEN)
.endObject()
.close();
OAuthResult result = OAuthClient.readResponse(source.read(), 1111);
Assert.assertEquals(ACCESS_TOKEN, result.accessToken);
Assert.assertEquals(REFRESH_TOKEN, result.refreshToken);
Assert.assertEquals(1111 + 3600000, result.expirationTimeMs);
}
}
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.devtools_bridge.gcd;
import android.test.InstrumentationTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import junit.framework.Assert;
import org.chromium.components.devtools_bridge.util.TestSource;
/**
* Tests for {@link MessageReaderTest}.
*/
public class MessageReaderTest extends InstrumentationTestCase {
private static final String DEVICE_ID = "4ac8a0f8-??????????????-192e2727710d";
private static final String ROBOT_ACCOUNT_EMAIL =
"2a3???????????????????????????87@clouddevices.gserviceaccount.com";
private static final String AUTHORIZATION_CODE =
"4/6V0jpup-????????????????????????????????????????????_e85kQI";
@SmallTest
public void testReadTicket() throws Exception {
TestSource source = new TestSource();
source.write().beginObject()
.name("kind").value("clouddevices#registrationTicket")
.name("id").value("p8hI4")
.name("deviceId").value(DEVICE_ID)
.name("creationTimeMs").value("1411029429794")
.name("expirationTimeMs").value("1411029669794")
.endObject().close();
String result = new MessageReader(source.read()).readTicketId();
Assert.assertEquals("p8hI4", result);
}
@SmallTest
public void testReadCredential() throws Exception {
TestSource source = new TestSource();
source.write().beginObject()
.name("kind").value("clouddevices#registrationTicket")
.name("id").value("p8hI4")
.name("deviceId").value(DEVICE_ID)
.name("userEmail").value("...@chromium.org")
.name("creationTimeMs").value("1411029429794")
.name("expirationTimeMs").value("1411029669794")
.name("robotAccountEmail").value(ROBOT_ACCOUNT_EMAIL)
.name("robotAccountAuthorizationCode").value(AUTHORIZATION_CODE)
.endObject().close();
InstanceCredential result = new MessageReader(source.read()).readInstanceCredential();
Assert.assertEquals(DEVICE_ID, result.id);
Assert.assertEquals(AUTHORIZATION_CODE, result.secret);
}
}
...@@ -6,17 +6,26 @@ package org.chromium.components.devtools_bridge.tests; ...@@ -6,17 +6,26 @@ package org.chromium.components.devtools_bridge.tests;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.widget.Button; import android.widget.Button;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams; import android.widget.LinearLayout.LayoutParams;
import android.widget.TextView; import android.widget.TextView;
import org.chromium.components.devtools_bridge.apiary.ApiaryClientFactory;
import org.chromium.components.devtools_bridge.apiary.TestApiaryClientFactory;
import org.chromium.components.devtools_bridge.ui.GCDRegistrationFragment;
/** /**
* Activity for testing devtools bridge. * Activity for testing devtools bridge.
*/ */
public class DebugActivity extends Activity { public class DebugActivity extends Activity {
private static final int LAYOUT_ID = 1000;
private LinearLayout mLayout; private LinearLayout mLayout;
@Override @Override
...@@ -24,15 +33,17 @@ public class DebugActivity extends Activity { ...@@ -24,15 +33,17 @@ public class DebugActivity extends Activity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
mLayout = new LinearLayout(this); mLayout = new LinearLayout(this);
mLayout.setId(LAYOUT_ID);
mLayout.setOrientation(LinearLayout.VERTICAL); mLayout.setOrientation(LinearLayout.VERTICAL);
String intro = "To test LocalTunnelBridge manually: \n" + String intro =
"1. Enable USB debugging.\n" + "To test LocalTunnelBridge manually: \n"
"2. Run ChromeShell along with this app.\n" + + "1. Enable USB debugging.\n"
"3. Start the LocalTunnelBridge.\n" + + "2. Run ChromeShell along with this app.\n"
"4. Connect the device to a desktop via USB.\n" + + "3. Start the LocalTunnelBridge.\n"
"5. Open chrome://inspect#devices on desktop Chrome.\n" + + "4. Connect the device to a desktop via USB.\n"
"6. Observe 2 identical Chrome Shells on the device."; + "5. Open chrome://inspect#devices on desktop Chrome.\n"
+ "6. Observe 2 identical Chrome Shells on the device.";
TextView textView = new TextView(this); TextView textView = new TextView(this);
textView.setText(intro); textView.setText(intro);
...@@ -45,6 +56,12 @@ public class DebugActivity extends Activity { ...@@ -45,6 +56,12 @@ public class DebugActivity extends Activity {
LayoutParams layoutParam = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams layoutParam = new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT); LayoutParams.MATCH_PARENT);
getFragmentManager()
.beginTransaction()
.add(LAYOUT_ID, new TestGCDRegistrationFragment())
.commit();
setContentView(mLayout, layoutParam); setContentView(mLayout, layoutParam);
} }
...@@ -69,5 +86,49 @@ public class DebugActivity extends Activity { ...@@ -69,5 +86,49 @@ public class DebugActivity extends Activity {
startService(intent); startService(intent);
} }
} }
private static class TestGCDRegistrationFragment
extends GCDRegistrationFragment implements View.OnClickListener {
private Button mButton;
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mButton = new Button(getActivity());
mButton.setOnClickListener(this);
updateText();
return mButton;
}
public void updateText() {
mButton.setText(isRegistered()
? "Unregister (registered for " + getOwner() + ")"
: "Register in GCD");
}
@Override
protected void onRegistrationStatusChange() {
updateText();
}
@Override
protected ApiaryClientFactory newClientFactory() {
return new TestApiaryClientFactory();
}
@Override
protected String generateDisplayName() {
return Build.MODEL + " Test app";
}
@Override
public void onClick(View v) {
if (!isRegistered()) {
register();
} else {
unregister();
}
}
}
} }
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.devtools_bridge.apiary;
/**
* Implementation of ApiaryClientFactory for manual testing.
*/
public class TestApiaryClientFactory extends ApiaryClientFactory {
@Override
public String getAPIKey() {
return "AIzaSyAI1TKbOdqMQ5TltbBT15V5XaIILnDadhI";
}
@Override
public String getOAuthClientId() {
return "287888336735-v2sniebgl82jgm8lpj6mesv982n505iq.apps.googleusercontent.com";
}
@Override
public String[] getGCMSenderIds() {
return new String[] { getGCMSenderId() };
}
public String getOAuthClientSecret() {
return "wdMQhGcQqOZmSNteEbqx0IfY";
}
public String getGCMSenderId() {
return "287888336735";
}
}
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.devtools_bridge.util;
import android.util.JsonReader;
import android.util.JsonWriter;
import java.io.StringReader;
import java.io.StringWriter;
/**
* Helper class for testing JSON-based readers.
*/
public class TestSource {
private final StringWriter mSource;
private final JsonWriter mSourceWriter;
public TestSource() {
mSource = new StringWriter();
mSourceWriter = new JsonWriter(mSource);
}
public JsonReader read() {
return new JsonReader(new StringReader(mSource.toString()));
}
public JsonWriter write() {
return mSourceWriter;
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment