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

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

Issue 165743002: Implement account switcher as action-bar navigation spinner. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: rebase Created 6 years, 10 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 unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright 2013 The Chromium Authors. All rights reserved. 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 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 package org.chromium.chromoting; 5 package org.chromium.chromoting;
6 6
7 import android.accounts.Account; 7 import android.accounts.Account;
8 import android.accounts.AccountManager; 8 import android.accounts.AccountManager;
9 import android.accounts.AccountManagerCallback; 9 import android.accounts.AccountManagerCallback;
10 import android.accounts.AccountManagerFuture; 10 import android.accounts.AccountManagerFuture;
11 import android.accounts.AuthenticatorException; 11 import android.accounts.AuthenticatorException;
12 import android.accounts.OperationCanceledException; 12 import android.accounts.OperationCanceledException;
13 import android.app.ActionBar;
13 import android.app.Activity; 14 import android.app.Activity;
14 import android.app.ProgressDialog; 15 import android.app.ProgressDialog;
15 import android.content.DialogInterface; 16 import android.content.DialogInterface;
16 import android.content.Intent; 17 import android.content.Intent;
17 import android.content.SharedPreferences; 18 import android.content.SharedPreferences;
19 import android.content.res.Configuration;
18 import android.os.Bundle; 20 import android.os.Bundle;
19 import android.util.Log; 21 import android.util.Log;
20 import android.view.Menu; 22 import android.view.Menu;
21 import android.view.MenuItem; 23 import android.view.MenuItem;
22 import android.widget.ArrayAdapter; 24 import android.widget.ArrayAdapter;
23 import android.widget.ListView; 25 import android.widget.ListView;
24 import android.widget.TextView; 26 import android.widget.TextView;
25 import android.widget.Toast; 27 import android.widget.Toast;
26 28
27 import org.chromium.chromoting.jni.JniInterface; 29 import org.chromium.chromoting.jni.JniInterface;
28 30
29 import java.io.IOException; 31 import java.io.IOException;
30 import java.util.Arrays; 32 import java.util.Arrays;
31 33
32 /** 34 /**
33 * The user interface for querying and displaying a user's host list from the di rectory server. It 35 * The user interface for querying and displaying a user's host list from the di rectory server. It
34 * also requests and renews authentication tokens using the system account manag er. 36 * also requests and renews authentication tokens using the system account manag er.
35 */ 37 */
36 public class Chromoting extends Activity implements JniInterface.ConnectionListe ner, 38 public class Chromoting extends Activity implements JniInterface.ConnectionListe ner,
37 AccountManagerCallback<Bundle>, HostListLoader.Callback { 39 AccountManagerCallback<Bundle>, ActionBar.OnNavigationListener,
40 HostListLoader.Callback {
38 /** Only accounts of this type will be selectable for authentication. */ 41 /** Only accounts of this type will be selectable for authentication. */
39 private static final String ACCOUNT_TYPE = "com.google"; 42 private static final String ACCOUNT_TYPE = "com.google";
40 43
41 /** Scopes at which the authentication token we request will be valid. */ 44 /** Scopes at which the authentication token we request will be valid. */
42 private static final String TOKEN_SCOPE = "oauth2:https://www.googleapis.com /auth/chromoting " + 45 private static final String TOKEN_SCOPE = "oauth2:https://www.googleapis.com /auth/chromoting " +
43 "https://www.googleapis.com/auth/googletalk"; 46 "https://www.googleapis.com/auth/googletalk";
44 47
45 /** User's account details. */ 48 /** User's account details. */
46 private Account mAccount; 49 private Account mAccount;
47 50
51 /** List of accounts on the system. */
52 private Account[] mAccounts;
53
48 /** Account auth token. */ 54 /** Account auth token. */
49 private String mToken; 55 private String mToken;
50 56
51 /** Helper for fetching the host list. */ 57 /** Helper for fetching the host list. */
52 private HostListLoader mHostListLoader; 58 private HostListLoader mHostListLoader;
53 59
54 /** List of hosts. */ 60 /** List of hosts. */
55 private HostInfo[] mHosts; 61 private HostInfo[] mHosts;
56 62
57 /** Refresh button. */ 63 /** Refresh button. */
58 private MenuItem mRefreshButton; 64 private MenuItem mRefreshButton;
59 65
60 /** Account switcher. */
61 private MenuItem mAccountSwitcher;
62
63 /** Greeting at the top of the displayed list. */ 66 /** Greeting at the top of the displayed list. */
64 private TextView mGreeting; 67 private TextView mGreeting;
65 68
66 /** Host list as it appears to the user. */ 69 /** Host list as it appears to the user. */
67 private ListView mList; 70 private ListView mList;
68 71
69 /** Dialog for reporting connection progress. */ 72 /** Dialog for reporting connection progress. */
70 private ProgressDialog mProgressIndicator; 73 private ProgressDialog mProgressIndicator;
71 74
72 /** 75 /**
73 * This is set when receiving an authentication error from the HostListLoade r. If that occurs, 76 * This is set when receiving an authentication error from the HostListLoade r. If that occurs,
74 * this flag is set and a fresh authentication token is fetched from the Acc ountsService, and 77 * this flag is set and a fresh authentication token is fetched from the Acc ountsService, and
75 * used to request the host list a second time. 78 * used to request the host list a second time.
76 */ 79 */
77 boolean mAlreadyTried; 80 boolean mTriedNewAuthToken;
78 81
79 /** 82 /**
80 * Called when the activity is first created. Loads the native library and r equests an 83 * Called when the activity is first created. Loads the native library and r equests an
81 * authentication token from the system. 84 * authentication token from the system.
82 */ 85 */
83 @Override 86 @Override
84 public void onCreate(Bundle savedInstanceState) { 87 public void onCreate(Bundle savedInstanceState) {
85 super.onCreate(savedInstanceState); 88 super.onCreate(savedInstanceState);
86 setContentView(R.layout.main); 89 setContentView(R.layout.main);
87 90
88 mAlreadyTried = false; 91 mTriedNewAuthToken = false;
89 mHostListLoader = new HostListLoader(); 92 mHostListLoader = new HostListLoader();
90 93
91 // Get ahold of our view widgets. 94 // Get ahold of our view widgets.
92 mGreeting = (TextView)findViewById(R.id.hostList_greeting); 95 mGreeting = (TextView)findViewById(R.id.hostList_greeting);
93 mList = (ListView)findViewById(R.id.hostList_chooser); 96 mList = (ListView)findViewById(R.id.hostList_chooser);
94 97
95 // Bring native components online. 98 // Bring native components online.
96 JniInterface.loadLibrary(this); 99 JniInterface.loadLibrary(this);
97 100
101 mAccounts = AccountManager.get(this).getAccountsByType(ACCOUNT_TYPE);
102 if (mAccounts.length == 0) {
103 // TODO(lambroslambrou): Show a dialog with message: "To use <App Na me>, you'll need
104 // to add a Google Account to your device." and two buttons: "Close" , "Add account".
105 // The "Add account" button should navigate to the system "Add a Goo gle Account"
106 // screen.
107 return;
108 }
109
98 SharedPreferences prefs = getPreferences(MODE_PRIVATE); 110 SharedPreferences prefs = getPreferences(MODE_PRIVATE);
111 int index = -1;
99 if (prefs.contains("account_name") && prefs.contains("account_type")) { 112 if (prefs.contains("account_name") && prefs.contains("account_type")) {
100 // Perform authentication using saved account selection.
101 mAccount = new Account(prefs.getString("account_name", null), 113 mAccount = new Account(prefs.getString("account_name", null),
102 prefs.getString("account_type", null)); 114 prefs.getString("account_type", null));
103 AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, t his, this, null); 115 index = Arrays.asList(mAccounts).indexOf(mAccount);
104 if (mAccountSwitcher != null) { 116 }
105 mAccountSwitcher.setTitle(mAccount.name); 117 if (index == -1) {
106 } 118 // Preference not loaded, or does not correspond to a valid account, so just pick the
119 // first account arbitrarily.
120 index = 0;
121 mAccount = mAccounts[0];
122 }
123
124 if (mAccounts.length == 1) {
125 getActionBar().setDisplayShowTitleEnabled(true);
126 getActionBar().setSubtitle(mAccount.name);
107 } else { 127 } else {
108 // Request auth callback once user has chosen an account. 128 AccountsAdapter adapter = new AccountsAdapter(this, mAccounts);
109 Log.i("auth", "Requesting auth token from system"); 129 getActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
110 AccountManager.get(this).getAuthTokenByFeatures(ACCOUNT_TYPE, TOKEN_ SCOPE, null, this, 130 getActionBar().setListNavigationCallbacks(adapter, this);
111 null, null, this, null); 131 getActionBar().setSelectedNavigationItem(index);
112 } 132 }
133
134 refreshHostList();
113 } 135 }
114 136
115 /** Called when the activity is finally finished. */ 137 /** Called when the activity is finally finished. */
116 @Override 138 @Override
117 public void onDestroy() { 139 public void onDestroy() {
118 super.onDestroy(); 140 super.onDestroy();
119 JniInterface.disconnectFromHost(); 141 JniInterface.disconnectFromHost();
120 } 142 }
121 143
144 /** Called when the display is rotated (as registered in the manifest). */
145 @Override
146 public void onConfigurationChanged(Configuration newConfig) {
147 super.onConfigurationChanged(newConfig);
148
149 // Reload the spinner resources, since the font sizes are dependent on t he screen
150 // orientation.
151 if (mAccounts.length != 1) {
152 AccountsAdapter adapter = new AccountsAdapter(this, mAccounts);
153 getActionBar().setListNavigationCallbacks(adapter, this);
154 }
155 }
156
122 /** Called to initialize the action bar. */ 157 /** Called to initialize the action bar. */
123 @Override 158 @Override
124 public boolean onCreateOptionsMenu(Menu menu) { 159 public boolean onCreateOptionsMenu(Menu menu) {
125 getMenuInflater().inflate(R.menu.chromoting_actionbar, menu); 160 getMenuInflater().inflate(R.menu.chromoting_actionbar, menu);
126 mRefreshButton = menu.findItem(R.id.actionbar_directoryrefresh); 161 mRefreshButton = menu.findItem(R.id.actionbar_directoryrefresh);
127 mAccountSwitcher = menu.findItem(R.id.actionbar_accountswitcher);
128
129 Account[] usableAccounts = AccountManager.get(this).getAccountsByType(AC COUNT_TYPE);
130 if (usableAccounts.length == 1 && usableAccounts[0].equals(mAccount)) {
131 // If we're using the only available account, don't offer account sw itching.
132 // (If there are *no* accounts available, clicking this allows you t o add a new one.)
133 mAccountSwitcher.setEnabled(false);
134 }
135 162
136 if (mAccount == null) { 163 if (mAccount == null) {
137 // If no account has been chosen, don't allow the user to refresh th e listing. 164 // If there is no account, don't allow the user to refresh the listi ng.
138 mRefreshButton.setEnabled(false); 165 mRefreshButton.setEnabled(false);
139 } else {
140 // If the user has picked an account, show its name directly on the account switcher.
141 mAccountSwitcher.setTitle(mAccount.name);
142 } 166 }
143 167
144 return super.onCreateOptionsMenu(menu); 168 return super.onCreateOptionsMenu(menu);
145 } 169 }
146 170
147 /** Called whenever an action bar button is pressed. */ 171 /** Called whenever an action bar button is pressed. */
148 @Override 172 @Override
149 public boolean onOptionsItemSelected(MenuItem item) { 173 public boolean onOptionsItemSelected(MenuItem item) {
150 mAlreadyTried = false; 174 refreshHostList();
151 if (item == mAccountSwitcher) {
152 // The account switcher triggers a listing of all available accounts .
153 AccountManager.get(this).getAuthTokenByFeatures(ACCOUNT_TYPE, TOKEN_ SCOPE, null, this,
154 null, null, this, null);
155 } else {
156 // The refresh button simply makes use of the currently-chosen accou nt.
157 AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, t his, this, null);
158 }
159
160 return true; 175 return true;
161 } 176 }
162 177
163 /** Called when the user taps on a host entry. */ 178 /** Called when the user taps on a host entry. */
164 public void connectToHost(HostInfo host) { 179 public void connectToHost(HostInfo host) {
165 if (host.jabberId.isEmpty() || host.publicKey.isEmpty()) { 180 if (host.jabberId.isEmpty() || host.publicKey.isEmpty()) {
166 // TODO(lambroslambrou): If these keys are not present, treat this a s a connection 181 // TODO(lambroslambrou): If these keys are not present, treat this a s a connection
167 // failure and reload the host list (see crbug.com/304719). 182 // failure and reload the host list (see crbug.com/304719).
168 Toast.makeText(this, getString(R.string.error_reading_host), 183 Toast.makeText(this, getString(R.string.error_reading_host),
169 Toast.LENGTH_LONG).show(); 184 Toast.LENGTH_LONG).show();
170 return; 185 return;
171 } 186 }
172 187
173 JniInterface.connectToHost(mAccount.name, mToken, host.jabberId, host.id , host.publicKey, 188 JniInterface.connectToHost(mAccount.name, mToken, host.jabberId, host.id , host.publicKey,
174 this); 189 this);
175 } 190 }
176 191
192 private void refreshHostList() {
193 mTriedNewAuthToken = false;
194
195 // The refresh button simply makes use of the currently-chosen account.
196 AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, this, this, null);
197 }
198
177 @Override 199 @Override
178 public void run(AccountManagerFuture<Bundle> future) { 200 public void run(AccountManagerFuture<Bundle> future) {
179 Log.i("auth", "User finished with auth dialogs"); 201 Log.i("auth", "User finished with auth dialogs");
180 Bundle result = null; 202 Bundle result = null;
181 String explanation = null; 203 String explanation = null;
182 try { 204 try {
183 // Here comes our auth token from the Android system. 205 // Here comes our auth token from the Android system.
184 result = future.getResult(); 206 result = future.getResult();
207 String authToken = result.getString(AccountManager.KEY_AUTHTOKEN);
208 Log.i("auth", "Received an auth token from system");
209
210 mToken = authToken;
211
212 mHostListLoader.retrieveHostList(authToken, this);
185 } catch (OperationCanceledException ex) { 213 } catch (OperationCanceledException ex) {
186 explanation = getString(R.string.error_auth_canceled); 214 explanation = getString(R.string.error_auth_canceled);
187 } catch (AuthenticatorException ex) { 215 } catch (AuthenticatorException ex) {
188 explanation = getString(R.string.error_no_accounts); 216 explanation = getString(R.string.error_no_accounts);
189 } catch (IOException ex) { 217 } catch (IOException ex) {
190 explanation = getString(R.string.error_bad_connection); 218 explanation = getString(R.string.error_bad_connection);
191 } 219 }
192 220
193 if (result == null) { 221 if (result == null) {
194 Toast.makeText(this, explanation, Toast.LENGTH_LONG).show(); 222 Toast.makeText(this, explanation, Toast.LENGTH_LONG).show();
195 return; 223 return;
196 } 224 }
197 225
198 String accountName = result.getString(AccountManager.KEY_ACCOUNT_NAME);
199 String accountType = result.getString(AccountManager.KEY_ACCOUNT_TYPE);
200 String authToken = result.getString(AccountManager.KEY_AUTHTOKEN); 226 String authToken = result.getString(AccountManager.KEY_AUTHTOKEN);
201 Log.i("auth", "Received an auth token from system"); 227 Log.i("auth", "Received an auth token from system");
202 228
203 mAccount = new Account(accountName, accountType);
204 mToken = authToken; 229 mToken = authToken;
205 getPreferences(MODE_PRIVATE).edit().putString("account_name", accountNam e).
206 putString("account_type", accountType).apply();
207 230
208 mHostListLoader.retrieveHostList(authToken, this); 231 mHostListLoader.retrieveHostList(authToken, this);
209 } 232 }
210 233
211 @Override 234 @Override
235 public boolean onNavigationItemSelected(int itemPosition, long itemId) {
236 mAccount = mAccounts[itemPosition];
237
238 getPreferences(MODE_PRIVATE).edit().putString("account_name", mAccount.n ame).
239 putString("account_type", mAccount.type).apply();
240
241 refreshHostList();
242 return true;
243 }
244
245 @Override
212 public void onHostListReceived(HostInfo[] hosts) { 246 public void onHostListReceived(HostInfo[] hosts) {
213 // Store a copy of the array, so that it can't be mutated by the HostLis tLoader. HostInfo 247 // Store a copy of the array, so that it can't be mutated by the HostLis tLoader. HostInfo
214 // is an immutable type, so a shallow copy of the array is sufficient he re. 248 // is an immutable type, so a shallow copy of the array is sufficient he re.
215 mHosts = Arrays.copyOf(hosts, hosts.length); 249 mHosts = Arrays.copyOf(hosts, hosts.length);
216 updateUi(); 250 updateUi();
217 } 251 }
218 252
219 @Override 253 @Override
220 public void onError(HostListLoader.Error error) { 254 public void onError(HostListLoader.Error error) {
221 String explanation = null; 255 String explanation = null;
(...skipping 15 matching lines...) Expand all
237 return; 271 return;
238 } 272 }
239 273
240 if (explanation != null) { 274 if (explanation != null) {
241 Toast.makeText(this, explanation, Toast.LENGTH_LONG).show(); 275 Toast.makeText(this, explanation, Toast.LENGTH_LONG).show();
242 return; 276 return;
243 } 277 }
244 278
245 // This is the AUTH_FAILED case. 279 // This is the AUTH_FAILED case.
246 280
247 if (!mAlreadyTried) { 281 if (!mTriedNewAuthToken) {
248 // This was our first connection attempt. 282 // This was our first connection attempt.
249 283
250 AccountManager authenticator = AccountManager.get(this); 284 AccountManager authenticator = AccountManager.get(this);
251 mAlreadyTried = true; 285 mTriedNewAuthToken = true;
252 286
253 Log.w("auth", "Requesting renewal of rejected auth token"); 287 Log.w("auth", "Requesting renewal of rejected auth token");
254 authenticator.invalidateAuthToken(mAccount.type, mToken); 288 authenticator.invalidateAuthToken(mAccount.type, mToken);
255 mToken = null; 289 mToken = null;
256 authenticator.getAuthToken(mAccount, TOKEN_SCOPE, null, this, this, null); 290 authenticator.getAuthToken(mAccount, TOKEN_SCOPE, null, this, this, null);
257 291
258 // We're not in an error state *yet*. 292 // We're not in an error state *yet*.
259 return; 293 return;
260 } else { 294 } else {
261 // Authentication truly failed. 295 // Authentication truly failed.
262 Log.e("auth", "Fresh auth token was also rejected"); 296 Log.e("auth", "Fresh auth token was also rejected");
263 explanation = getString(R.string.error_auth_failed); 297 explanation = getString(R.string.error_auth_failed);
264 Toast.makeText(this, explanation, Toast.LENGTH_LONG).show(); 298 Toast.makeText(this, explanation, Toast.LENGTH_LONG).show();
265 } 299 }
266 } 300 }
267 301
268 /** 302 /**
269 * Updates the infotext and host list display. 303 * Updates the infotext and host list display.
270 */ 304 */
271 private void updateUi() { 305 private void updateUi() {
272 mRefreshButton.setEnabled(mAccount != null); 306 mRefreshButton.setEnabled(mAccount != null);
273 if (mAccount != null) {
274 mAccountSwitcher.setTitle(mAccount.name);
275 }
276 307
277 if (mHosts == null) { 308 if (mHosts == null) {
278 mGreeting.setText(getString(R.string.inst_empty_list)); 309 mGreeting.setText(getString(R.string.inst_empty_list));
279 mList.setAdapter(null); 310 mList.setAdapter(null);
280 return; 311 return;
281 } 312 }
282 313
283 mGreeting.setText(getString(R.string.inst_host_list)); 314 mGreeting.setText(getString(R.string.inst_host_list));
284 315
285 ArrayAdapter<HostInfo> displayer = new HostListAdapter(this, R.layout.ho st, mHosts); 316 ArrayAdapter<HostInfo> displayer = new HostListAdapter(this, R.layout.ho st, mHosts);
(...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after
341 // Unreachable, but required by Google Java style and findbugs. 372 // Unreachable, but required by Google Java style and findbugs.
342 assert false : "Unreached"; 373 assert false : "Unreached";
343 } 374 }
344 375
345 if (dismissProgress && mProgressIndicator != null) { 376 if (dismissProgress && mProgressIndicator != null) {
346 mProgressIndicator.dismiss(); 377 mProgressIndicator.dismiss();
347 mProgressIndicator = null; 378 mProgressIndicator = null;
348 } 379 }
349 } 380 }
350 } 381 }
OLDNEW
« no previous file with comments | « remoting/android/java/src/org/chromium/chromoting/AccountsAdapter.java ('k') | remoting/remoting_android.gypi » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698