| 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;
|
| + }
|
| + }
|
| +}
|
|
|