OLD | NEW |
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 Loading... |
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 Loading... |
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 } |
OLD | NEW |