Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1700)

Unified Diff: remoting/android/java/src/org/chromium/chromoting/Chromoting.java

Issue 19506004: Add the beginnings of a Chromoting Android app (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Minimize monitor use, concatenate using StringBuilder, tune host list spacing, make build target re… Created 7 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: remoting/android/java/src/org/chromium/chromoting/Chromoting.java
diff --git a/remoting/android/java/src/org/chromium/chromoting/Chromoting.java b/remoting/android/java/src/org/chromium/chromoting/Chromoting.java
new file mode 100644
index 0000000000000000000000000000000000000000..23e46777007ae0bdbcbeb8f6f25ef76226e2d140
--- /dev/null
+++ b/remoting/android/java/src/org/chromium/chromoting/Chromoting.java
@@ -0,0 +1,317 @@
+// Copyright 2013 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.chromoting;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerCallback;
+import android.accounts.AccountManagerFuture;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.text.Html;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+import android.widget.ListView;
+import android.widget.Toast;
+
+import org.chromium.chromoting.jni.JniInterface;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Scanner;
+
+/**
+ * The user interface for querying and displaying a user's host list from the directory server. It
+ * also requests and renews authentication tokens using the system account manager.
+ */
+public class Chromoting extends Activity {
+ /** Only accounts of this type will be selectable for authentication. */
+ private static final String ACCOUNT_TYPE = "com.google";
+
+ /** Scopes at which the authentication token we request will be valid. */
+ private static final String TOKEN_SCOPE = "oauth2:https://www.googleapis.com/auth/chromoting " +
+ "https://www.googleapis.com/auth/googletalk";
+
+ /** Path from which to download a user's host list JSON object. */
+ private static final String HOST_LIST_PATH =
+ "https://www.googleapis.com/chromoting/v1/@me/hosts?key=";
+
+ /** Color to use for hosts that are online. */
+ private static final String HOST_COLOR_ONLINE = "green";
+
+ /** Color to use for hosts that are offline. */
+ private static final String HOST_COLOR_OFFLINE = "red";
+
+ /** User's account details. */
+ Account mAccount;
+
+ /** Account auth token. */
+ String mToken;
+
+ /** List of hosts. */
+ JSONArray mHosts;
+
+ /** Greeting at the top of the displayed list. */
+ TextView mGreeting;
+
+ /** Host list as it appears to the user. */
+ ListView mList;
+
+ /** Callback handler to be used for network operations. */
+ Handler mNetwork;
+
+ /**
+ * Called when the activity is first created. Loads the native library and requests an
+ * authentication token from the system.
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main);
+
+ // Get ahold of our view widgets.
+ mGreeting = (TextView)findViewById(R.id.hostList_greeting);
+ mList = (ListView)findViewById(R.id.hostList_chooser);
+
+ // Bring native components online.
+ JniInterface.loadLibrary(this);
+
+ // Thread responsible for downloading/displaying host list.
+ HandlerThread thread = new HandlerThread("auth_callback");
+ thread.start();
+ mNetwork = new Handler(thread.getLooper());
+
+ // Request callback once user has chosen an account.
+ Log.i("auth", "Requesting auth token from system");
+ AccountManager.get(this).getAuthTokenByFeatures(
+ ACCOUNT_TYPE,
+ TOKEN_SCOPE,
+ null,
+ this,
+ null,
+ null,
+ new HostListDirectoryGrabber(this),
+ mNetwork
+ );
+ }
+
+ /** Called when the activity is finally finished. */
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ JniInterface.disconnectFromHost();
+ }
+
+ /**
+ * Processes the authentication token once the system provides it. Once in possession of such a
+ * token, attempts to request a host list from the directory server. In case of a bad response,
+ * this is retried once in case the system's cached auth token had expired.
+ */
+ private class HostListDirectoryGrabber implements AccountManagerCallback<Bundle> {
+ /** Whether authentication has already been attempted. */
+ private boolean mAlreadyTried;
+
+ /** Communication with the screen. */
+ private Activity mUi;
+
+ /** Constructor. */
+ public HostListDirectoryGrabber(Activity ui) {
+ mAlreadyTried = false;
+ mUi = ui;
+ }
+
+ /**
+ * Retrieves the host list from the directory server. This method performs
+ * network operations and must be run an a non-UI thread.
+ */
+ @Override
+ public void run(AccountManagerFuture<Bundle> future) {
+ Log.i("auth", "User finished with auth dialogs");
+ mAlreadyTried = true;
+ try {
+ // Here comes our auth token from the Android system.
+ Bundle result = future.getResult();
+ Log.i("auth", "Received an auth token from system");
+ mAccount = new Account(result.getString(AccountManager.KEY_ACCOUNT_NAME),
+ result.getString(AccountManager.KEY_ACCOUNT_TYPE));
+ mToken = result.getString(AccountManager.KEY_AUTHTOKEN);
+
+ // Send our HTTP request to the directory server.
+ URLConnection link =
+ new URL(HOST_LIST_PATH + JniInterface.getApiKey()).openConnection();
+ link.addRequestProperty("client_id", JniInterface.getClientId());
+ link.addRequestProperty("client_secret", JniInterface.getClientSecret());
+ link.setRequestProperty("Authorization", "OAuth " + mToken);
+
+ // Listen for the server to respond.
+ StringBuilder response = new StringBuilder();
+ Scanner incoming = new Scanner(link.getInputStream());
+ Log.i("auth", "Successfully authenticated to directory server");
+ while (incoming.hasNext()) {
+ response.append(incoming.nextLine());
+ }
+ incoming.close();
+
+ // Interpret what the directory server told us.
+ JSONObject data = new JSONObject(String.valueOf(response)).getJSONObject("data");
+ mHosts = data.getJSONArray("items");
+ Log.i("hostlist", "Received host listing from directory server");
+
+ // Share our findings with the user.
+ runOnUiThread(new HostListDisplayer(mUi));
+ }
+ catch(Exception ex) {
+ // Assemble error message to display to the user.
+ String explanation = getString(R.string.error_unknown);
+ if (ex instanceof OperationCanceledException) {
+ explanation = getString(R.string.error_auth_canceled);
+ } else if (ex instanceof AuthenticatorException) {
+ explanation = getString(R.string.error_no_accounts);
+ } else if (ex instanceof IOException) {
+ if (!mAlreadyTried) { // This was our first connection attempt.
+ Log.w("auth", "Unable to authenticate with (expired?) token");
+
+ // Ask system to renew the auth token in case it expired.
+ AccountManager authenticator = AccountManager.get(mUi);
+ authenticator.invalidateAuthToken(mAccount.type, mToken);
+ Log.i("auth", "Requesting auth token renewal");
+ authenticator.getAuthToken(
+ mAccount, TOKEN_SCOPE, null, mUi, this, mNetwork);
+
+ // We're not in an error state *yet.*
+ return;
+ } else { // Authentication truly failed.
+ Log.e("auth", "Fresh auth token was also rejected");
+ explanation = getString(R.string.error_auth_failed);
+ }
+ } else if (ex instanceof JSONException) {
+ explanation = getString(R.string.error_unexpected_response);
+ }
+
+ Log.w("auth", ex);
+ Toast.makeText(mUi, explanation, Toast.LENGTH_LONG).show();
+
+ // Close the application.
+ finish();
+ }
+ }
+ }
+
+ /** Formats the host list and offers it to the user. */
+ private class HostListDisplayer implements Runnable {
+ /** Communication with the screen. */
+ private Activity mUi;
+
+ /** Constructor. */
+ public HostListDisplayer(Activity ui) {
+ mUi = ui;
+ }
+
+ /**
+ * Updates the infotext and host list display.
+ * This method affects the UI and must be run on its same thread.
+ */
+ @Override
+ public void run() {
+ mGreeting.setText(getString(R.string.inst_host_list));
+
+ ArrayAdapter<JSONObject> displayer = new HostListAdapter(mUi, R.layout.host);
+ Log.i("hostlist", "About to populate host list display");
+ try {
+ int index = 0;
+ while (!mHosts.isNull(index)) {
+ displayer.add(mHosts.getJSONObject(index));
+ ++index;
+ }
+ mList.setAdapter(displayer);
+ }
+ catch(JSONException ex) {
+ Log.w("hostlist", ex);
+ Toast.makeText(
+ mUi, getString(R.string.error_cataloging_hosts), Toast.LENGTH_LONG).show();
+
+ // Close the application.
+ finish();
+ }
+ }
+ }
+
+ /** Describes the appearance and behavior of each host list entry. */
+ private class HostListAdapter extends ArrayAdapter<JSONObject> {
+ /** Constructor. */
+ public HostListAdapter(Context context, int textViewResourceId) {
+ super(context, textViewResourceId);
+ }
+
+ /** Generates a View corresponding to this particular host. */
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ TextView target = (TextView)super.getView(position, convertView, parent);
+
+ try {
+ final JSONObject host = getItem(position);
+ target.setText(Html.fromHtml(host.getString("hostName") + " (<font color = \"" +
+ (host.getString("status").equals("ONLINE") ? HOST_COLOR_ONLINE :
+ HOST_COLOR_OFFLINE) + "\">" + host.getString("status") + "</font>)"));
+
+ if (host.getString("status").equals("ONLINE")) { // Host is online.
+ target.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ try {
+ JniInterface.connectToHost(
+ mAccount.name, mToken, host.getString("jabberId"),
+ host.getString("hostId"), host.getString("publicKey"),
+ new Runnable() {
+ @Override
+ public void run() {
+ // TODO(solb) Start an Activity to display the desktop.
+ }
+ });
+ }
+ catch(JSONException ex) {
+ Log.w("host", ex);
+ Toast.makeText(getContext(),
+ getString(R.string.error_reading_host),
+ Toast.LENGTH_LONG).show();
+
+ // Close the application.
+ finish();
+ }
+ }
+ });
+ } else { // Host is offline.
+ // Disallow interaction with this entry.
+ target.setEnabled(false);
+ }
+ }
+ catch(JSONException ex) {
+ Log.w("hostlist", ex);
+ Toast.makeText(getContext(),
+ getString(R.string.error_displaying_host),
+ Toast.LENGTH_LONG).show();
+
+ // Close the application.
+ finish();
+ }
+
+ return target;
+ }
+ }
+}
« no previous file with comments | « remoting/android/java/AndroidManifest.xml ('k') | remoting/android/java/src/org/chromium/chromoting/jni/JniInterface.java » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698