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