Home | History | Annotate | Download | only in chromoting
      1 // Copyright 2013 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 package org.chromium.chromoting;
      6 
      7 import android.accounts.Account;
      8 import android.accounts.AccountManager;
      9 import android.accounts.AccountManagerCallback;
     10 import android.accounts.AccountManagerFuture;
     11 import android.accounts.AuthenticatorException;
     12 import android.accounts.OperationCanceledException;
     13 import android.app.Activity;
     14 import android.content.Context;
     15 import android.content.Intent;
     16 import android.content.SharedPreferences;
     17 import android.os.Bundle;
     18 import android.os.Handler;
     19 import android.os.HandlerThread;
     20 import android.text.Html;
     21 import android.util.Log;
     22 import android.view.Menu;
     23 import android.view.MenuItem;
     24 import android.view.View;
     25 import android.view.ViewGroup;
     26 import android.widget.ArrayAdapter;
     27 import android.widget.TextView;
     28 import android.widget.ListView;
     29 import android.widget.Toast;
     30 
     31 import org.chromium.chromoting.jni.JniInterface;
     32 import org.json.JSONArray;
     33 import org.json.JSONException;
     34 import org.json.JSONObject;
     35 
     36 import java.io.IOException;
     37 import java.net.URL;
     38 import java.net.URLConnection;
     39 import java.util.Scanner;
     40 
     41 /**
     42  * The user interface for querying and displaying a user's host list from the directory server. It
     43  * also requests and renews authentication tokens using the system account manager.
     44  */
     45 public class Chromoting extends Activity {
     46     /** Only accounts of this type will be selectable for authentication. */
     47     private static final String ACCOUNT_TYPE = "com.google";
     48 
     49     /** Scopes at which the authentication token we request will be valid. */
     50     private static final String TOKEN_SCOPE = "oauth2:https://www.googleapis.com/auth/chromoting " +
     51             "https://www.googleapis.com/auth/googletalk";
     52 
     53     /** Path from which to download a user's host list JSON object. */
     54     private static final String HOST_LIST_PATH =
     55             "https://www.googleapis.com/chromoting/v1/@me/hosts?key=";
     56 
     57     /** Color to use for hosts that are online. */
     58     private static final String HOST_COLOR_ONLINE = "green";
     59 
     60     /** Color to use for hosts that are offline. */
     61     private static final String HOST_COLOR_OFFLINE = "red";
     62 
     63     /** User's account details. */
     64     private Account mAccount;
     65 
     66     /** Account auth token. */
     67     private String mToken;
     68 
     69     /** List of hosts. */
     70     private JSONArray mHosts;
     71 
     72     /** Refresh button. */
     73     private MenuItem mRefreshButton;
     74 
     75     /** Account switcher. */
     76     private MenuItem mAccountSwitcher;
     77 
     78     /** Greeting at the top of the displayed list. */
     79     private TextView mGreeting;
     80 
     81     /** Host list as it appears to the user. */
     82     private ListView mList;
     83 
     84     /** Callback handler to be used for network operations. */
     85     private Handler mNetwork;
     86 
     87     /**
     88      * Called when the activity is first created. Loads the native library and requests an
     89      * authentication token from the system.
     90      */
     91     @Override
     92     public void onCreate(Bundle savedInstanceState) {
     93         super.onCreate(savedInstanceState);
     94         setContentView(R.layout.main);
     95 
     96         // Get ahold of our view widgets.
     97         mGreeting = (TextView)findViewById(R.id.hostList_greeting);
     98         mList = (ListView)findViewById(R.id.hostList_chooser);
     99 
    100         // Bring native components online.
    101         JniInterface.loadLibrary(this);
    102 
    103         // Thread responsible for downloading/displaying host list.
    104         HandlerThread thread = new HandlerThread("auth_callback");
    105         thread.start();
    106         mNetwork = new Handler(thread.getLooper());
    107 
    108         SharedPreferences prefs = getPreferences(MODE_PRIVATE);
    109         if (prefs.contains("account_name") && prefs.contains("account_type")) {
    110             // Perform authentication using saved account selection.
    111             mAccount = new Account(prefs.getString("account_name", null),
    112                     prefs.getString("account_type", null));
    113             AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, this,
    114                     new HostListDirectoryGrabber(this), mNetwork);
    115             if (mAccountSwitcher != null) {
    116                 mAccountSwitcher.setTitle(mAccount.name);
    117             }
    118         } else {
    119             // Request auth callback once user has chosen an account.
    120             Log.i("auth", "Requesting auth token from system");
    121             AccountManager.get(this).getAuthTokenByFeatures(
    122                     ACCOUNT_TYPE,
    123                     TOKEN_SCOPE,
    124                     null,
    125                     this,
    126                     null,
    127                     null,
    128                     new HostListDirectoryGrabber(this),
    129                     mNetwork
    130                 );
    131         }
    132     }
    133 
    134     /** Called when the activity is finally finished. */
    135     @Override
    136     public void onDestroy() {
    137         super.onDestroy();
    138         JniInterface.disconnectFromHost();
    139     }
    140 
    141     /** Called to initialize the action bar. */
    142     @Override
    143     public boolean onCreateOptionsMenu(Menu menu) {
    144         getMenuInflater().inflate(R.menu.chromoting_actionbar, menu);
    145         mRefreshButton = menu.findItem(R.id.actionbar_directoryrefresh);
    146         mAccountSwitcher = menu.findItem(R.id.actionbar_accountswitcher);
    147 
    148         Account[] usableAccounts = AccountManager.get(this).getAccountsByType(ACCOUNT_TYPE);
    149         if (usableAccounts.length == 1 && usableAccounts[0].equals(mAccount)) {
    150             // If we're using the only available account, don't offer account switching.
    151             // (If there are *no* accounts available, clicking this allows you to add a new one.)
    152             mAccountSwitcher.setEnabled(false);
    153         }
    154 
    155         if (mAccount == null) {
    156             // If no account has been chosen, don't allow the user to refresh the listing.
    157             mRefreshButton.setEnabled(false);
    158         } else {
    159             // If the user has picked an account, show its name directly on the account switcher.
    160             mAccountSwitcher.setTitle(mAccount.name);
    161         }
    162 
    163         return super.onCreateOptionsMenu(menu);
    164     }
    165 
    166     /** Called whenever an action bar button is pressed. */
    167     @Override
    168     public boolean onOptionsItemSelected(MenuItem item) {
    169         if (item == mAccountSwitcher) {
    170             // The account switcher triggers a listing of all available accounts.
    171             AccountManager.get(this).getAuthTokenByFeatures(
    172                     ACCOUNT_TYPE,
    173                     TOKEN_SCOPE,
    174                     null,
    175                     this,
    176                     null,
    177                     null,
    178                     new HostListDirectoryGrabber(this),
    179                     mNetwork
    180                 );
    181         }
    182         else {
    183             // The refresh button simply makes use of the currently-chosen account.
    184             AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, this,
    185                     new HostListDirectoryGrabber(this), mNetwork);
    186         }
    187 
    188         return true;
    189     }
    190 
    191     /**
    192      * Processes the authentication token once the system provides it. Once in possession of such a
    193      * token, attempts to request a host list from the directory server. In case of a bad response,
    194      * this is retried once in case the system's cached auth token had expired.
    195      */
    196     private class HostListDirectoryGrabber implements AccountManagerCallback<Bundle> {
    197         /** Whether authentication has already been attempted. */
    198         private boolean mAlreadyTried;
    199 
    200         /** Communication with the screen. */
    201         private Activity mUi;
    202 
    203         /** Constructor. */
    204         public HostListDirectoryGrabber(Activity ui) {
    205             mAlreadyTried = false;
    206             mUi = ui;
    207         }
    208 
    209         /**
    210          * Retrieves the host list from the directory server. This method performs
    211          * network operations and must be run an a non-UI thread.
    212          */
    213         @Override
    214         public void run(AccountManagerFuture<Bundle> future) {
    215             Log.i("auth", "User finished with auth dialogs");
    216             try {
    217                 // Here comes our auth token from the Android system.
    218                 Bundle result = future.getResult();
    219                 String accountName = result.getString(AccountManager.KEY_ACCOUNT_NAME);
    220                 String accountType = result.getString(AccountManager.KEY_ACCOUNT_TYPE);
    221                 String authToken = result.getString(AccountManager.KEY_AUTHTOKEN);
    222                 Log.i("auth", "Received an auth token from system");
    223 
    224                 synchronized (mUi) {
    225                     mAccount = new Account(accountName, accountType);
    226                     mToken = authToken;
    227                     getPreferences(MODE_PRIVATE).edit().putString("account_name", accountName).
    228                             putString("account_type", accountType).apply();
    229                 }
    230 
    231                 // Send our HTTP request to the directory server.
    232                 URLConnection link =
    233                         new URL(HOST_LIST_PATH + JniInterface.getApiKey()).openConnection();
    234                 link.addRequestProperty("client_id", JniInterface.getClientId());
    235                 link.addRequestProperty("client_secret", JniInterface.getClientSecret());
    236                 link.setRequestProperty("Authorization", "OAuth " + authToken);
    237 
    238                 // Listen for the server to respond.
    239                 StringBuilder response = new StringBuilder();
    240                 Scanner incoming = new Scanner(link.getInputStream());
    241                 Log.i("auth", "Successfully authenticated to directory server");
    242                 while (incoming.hasNext()) {
    243                     response.append(incoming.nextLine());
    244                 }
    245                 incoming.close();
    246 
    247                 // Interpret what the directory server told us.
    248                 JSONObject data = new JSONObject(String.valueOf(response)).getJSONObject("data");
    249                 mHosts = data.getJSONArray("items");
    250                 Log.i("hostlist", "Received host listing from directory server");
    251             } catch (RuntimeException ex) {
    252                 // Make sure any other failure is reported to the user (as an unknown error).
    253                 throw ex;
    254             } catch (Exception ex) {
    255                 // Assemble error message to display to the user.
    256                 String explanation = getString(R.string.error_unknown);
    257                 if (ex instanceof OperationCanceledException) {
    258                     explanation = getString(R.string.error_auth_canceled);
    259                 } else if (ex instanceof AuthenticatorException) {
    260                     explanation = getString(R.string.error_no_accounts);
    261                 } else if (ex instanceof IOException) {
    262                     if (!mAlreadyTried) {
    263                         // This was our first connection attempt.
    264 
    265                         synchronized (mUi) {
    266                             if (mAccount != null) {
    267                                 // We got an account, but couldn't log into it. We'll retry in case
    268                                 // the system's cached authentication token had already expired.
    269                                 AccountManager authenticator = AccountManager.get(mUi);
    270                                 mAlreadyTried = true;
    271 
    272                                 Log.w("auth", "Requesting renewal of rejected auth token");
    273                                 authenticator.invalidateAuthToken(mAccount.type, mToken);
    274                                 mToken = null;
    275                                 authenticator.getAuthToken(
    276                                         mAccount, TOKEN_SCOPE, null, mUi, this, mNetwork);
    277 
    278                                 // We're not in an error state *yet*.
    279                                 return;
    280                             }
    281                         }
    282 
    283                         // We didn't even get an account, so the auth server is likely unreachable.
    284                         explanation = getString(R.string.error_bad_connection);
    285                     } else {
    286                         // Authentication truly failed.
    287                         Log.e("auth", "Fresh auth token was also rejected");
    288                         explanation = getString(R.string.error_auth_failed);
    289                     }
    290                 } else if (ex instanceof JSONException) {
    291                     explanation = getString(R.string.error_unexpected_response);
    292                     runOnUiThread(new HostListDisplayer(mUi));
    293                 }
    294 
    295                 mHosts = null;
    296                 Log.w("auth", ex);
    297                 Toast.makeText(mUi, explanation, Toast.LENGTH_LONG).show();
    298             }
    299 
    300             // Share our findings with the user.
    301             runOnUiThread(new HostListDisplayer(mUi));
    302         }
    303     }
    304 
    305     /** Formats the host list and offers it to the user. */
    306     private class HostListDisplayer implements Runnable {
    307         /** Communication with the screen. */
    308         private Activity mUi;
    309 
    310         /** Constructor. */
    311         public HostListDisplayer(Activity ui) {
    312             mUi = ui;
    313         }
    314 
    315         /**
    316          * Updates the infotext and host list display.
    317          * This method affects the UI and must be run on its same thread.
    318          */
    319         @Override
    320         public void run() {
    321             synchronized (mUi) {
    322                 mRefreshButton.setEnabled(mAccount != null);
    323                 if (mAccount != null) {
    324                     mAccountSwitcher.setTitle(mAccount.name);
    325                 }
    326             }
    327 
    328             if (mHosts == null) {
    329                 mGreeting.setText(getString(R.string.inst_empty_list));
    330                 mList.setAdapter(null);
    331                 return;
    332             }
    333 
    334             mGreeting.setText(getString(R.string.inst_host_list));
    335 
    336             ArrayAdapter<JSONObject> displayer = new HostListAdapter(mUi, R.layout.host);
    337             Log.i("hostlist", "About to populate host list display");
    338             try {
    339                 int index = 0;
    340                 while (!mHosts.isNull(index)) {
    341                     displayer.add(mHosts.getJSONObject(index));
    342                     ++index;
    343                 }
    344                 mList.setAdapter(displayer);
    345             }
    346             catch(JSONException ex) {
    347                 Log.w("hostlist", ex);
    348                 Toast.makeText(
    349                         mUi, getString(R.string.error_cataloging_hosts), Toast.LENGTH_LONG).show();
    350 
    351                 // Close the application.
    352                 finish();
    353             }
    354         }
    355     }
    356 
    357     /** Describes the appearance and behavior of each host list entry. */
    358     private class HostListAdapter extends ArrayAdapter<JSONObject> {
    359         /** Constructor. */
    360         public HostListAdapter(Context context, int textViewResourceId) {
    361             super(context, textViewResourceId);
    362         }
    363 
    364         /** Generates a View corresponding to this particular host. */
    365         @Override
    366         public View getView(int position, View convertView, ViewGroup parent) {
    367             TextView target = (TextView)super.getView(position, convertView, parent);
    368 
    369             try {
    370                 final JSONObject host = getItem(position);
    371                 target.setText(Html.fromHtml(host.getString("hostName") + " (<font color = \"" +
    372                         (host.getString("status").equals("ONLINE") ? HOST_COLOR_ONLINE :
    373                         HOST_COLOR_OFFLINE) + "\">" + host.getString("status") + "</font>)"));
    374 
    375                 if (host.getString("status").equals("ONLINE")) {  // Host is online.
    376                     target.setOnClickListener(new View.OnClickListener() {
    377                             @Override
    378                             public void onClick(View v) {
    379                                 try {
    380                                     synchronized (getContext()) {
    381                                         JniInterface.connectToHost(mAccount.name, mToken,
    382                                                 host.getString("jabberId"),
    383                                                 host.getString("hostId"),
    384                                                 host.getString("publicKey"),
    385                                                 new Runnable() {
    386                                             @Override
    387                                             public void run() {
    388                                                 startActivity(
    389                                                         new Intent(getContext(), Desktop.class));
    390                                             }
    391                                         });
    392                                     }
    393                                 }
    394                                 catch(JSONException ex) {
    395                                     Log.w("host", ex);
    396                                     Toast.makeText(getContext(),
    397                                             getString(R.string.error_reading_host),
    398                                             Toast.LENGTH_LONG).show();
    399 
    400                                     // Close the application.
    401                                     finish();
    402                                 }
    403                             }
    404                         });
    405                 } else {  // Host is offline.
    406                     // Disallow interaction with this entry.
    407                     target.setEnabled(false);
    408                 }
    409             }
    410             catch(JSONException ex) {
    411                 Log.w("hostlist", ex);
    412                 Toast.makeText(getContext(),
    413                         getString(R.string.error_displaying_host),
    414                         Toast.LENGTH_LONG).show();
    415 
    416                 // Close the application.
    417                 finish();
    418             }
    419 
    420             return target;
    421         }
    422     }
    423 }
    424