diff options
author | lambroslambrou <lambroslambrou@chromium.org> | 2015-03-18 12:17:34 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-03-18 19:18:24 +0000 |
commit | d09f63e285e28e90743c16332b06bc89d17de1a5 (patch) | |
tree | 583e6b101b48f2c1bb413032874a83953c6f554f /remoting | |
parent | e20ec7cdb825c84c54392f7fa4dca9d2a1c05158 (diff) | |
download | chromium_src-d09f63e285e28e90743c16332b06bc89d17de1a5.zip chromium_src-d09f63e285e28e90743c16332b06bc89d17de1a5.tar.gz chromium_src-d09f63e285e28e90743c16332b06bc89d17de1a5.tar.bz2 |
Pull authentication UX code out of JniInterface
UX code does not belong in the JniInterface layer. This is a first step
towards refactoring the JNI layer away from static methods, into a
separate object whose lifetime is controlled by Java, which can be
mocked for testing.
Review URL: https://codereview.chromium.org/981783003
Cr-Commit-Position: refs/heads/master@{#321187}
Diffstat (limited to 'remoting')
4 files changed, 207 insertions, 133 deletions
diff --git a/remoting/android/java/src/org/chromium/chromoting/Chromoting.java b/remoting/android/java/src/org/chromium/chromoting/Chromoting.java index 8fdd14d..15a8b15 100644 --- a/remoting/android/java/src/org/chromium/chromoting/Chromoting.java +++ b/remoting/android/java/src/org/chromium/chromoting/Chromoting.java @@ -90,8 +90,11 @@ public class Chromoting extends ActionBarActivity implements JniInterface.Connec /** Dialog for reporting connection progress. */ private ProgressDialog mProgressIndicator; - /** Object for fetching OAuth2 access tokens from third party authorization servers. */ - private ThirdPartyTokenFetcher mTokenFetcher; + /** + * Helper used by SessionConnection for session authentication. Receives onNewIntent() + * notifications to handle third-party authentication. + */ + private SessionAuthenticator mAuthenticator; /** * This is set when receiving an authentication error from the HostListLoader. If that occurs, @@ -214,10 +217,9 @@ public class Chromoting extends ActionBarActivity implements JniInterface.Connec @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); - if (mTokenFetcher != null) { - if (mTokenFetcher.handleTokenFetched(intent)) { - mTokenFetcher = null; - } + + if (mAuthenticator != null) { + mAuthenticator.onNewIntent(intent); } } @@ -323,13 +325,11 @@ public class Chromoting extends ActionBarActivity implements JniInterface.Connec @Override public void onCancel(DialogInterface dialog) { JniInterface.disconnectFromHost(); - mTokenFetcher = null; } }); SessionConnector connector = new SessionConnector(this, this, mHostListLoader); - assert mTokenFetcher == null; - mTokenFetcher = createTokenFetcher(host); - connector.connectToHost(mAccount.name, mToken, host); + mAuthenticator = new SessionAuthenticator(this, host); + connector.connectToHost(mAccount.name, mToken, host, mAuthenticator); } private void refreshHostList() { @@ -510,28 +510,4 @@ public class Chromoting extends ActionBarActivity implements JniInterface.Connec mProgressIndicator = null; } } - - private ThirdPartyTokenFetcher createTokenFetcher(HostInfo host) { - ThirdPartyTokenFetcher.Callback callback = new ThirdPartyTokenFetcher.Callback() { - @Override - public void onTokenFetched(String code, String accessToken) { - // The native client sends the OAuth authorization code to the host as the token so - // that the host can obtain the shared secret from the third party authorization - // server. - String token = code; - - // The native client uses the OAuth access token as the shared secret to - // authenticate itself with the host using spake. - String sharedSecret = accessToken; - - JniInterface.onThirdPartyTokenFetched(token, sharedSecret); - } - }; - return new ThirdPartyTokenFetcher(this, host.getTokenUrlPatterns(), callback); - } - - public void fetchThirdPartyToken(String tokenUrl, String clientId, String scope) { - assert mTokenFetcher != null; - mTokenFetcher.fetchToken(tokenUrl, clientId, scope); - } } diff --git a/remoting/android/java/src/org/chromium/chromoting/SessionAuthenticator.java b/remoting/android/java/src/org/chromium/chromoting/SessionAuthenticator.java new file mode 100644 index 0000000..a18c2f4 --- /dev/null +++ b/remoting/android/java/src/org/chromium/chromoting/SessionAuthenticator.java @@ -0,0 +1,168 @@ +// Copyright 2015 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.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Build; +import android.view.KeyEvent; +import android.view.View; +import android.widget.CheckBox; +import android.widget.TextView; +import android.widget.Toast; + +import org.chromium.chromoting.jni.JniInterface; + +/** + * This class performs the user-interaction needed to authenticate the session connection. This + * includes showing the PIN prompt and requesting tokens for third-party authentication. + */ +public class SessionAuthenticator { + /** + * Application context used for getting user preferences, displaying UI, and fetching + * third-party tokens. + */ + private Chromoting mApplicationContext; + + /** Provides the tokenUrlPatterns for this host during fetchThirdPartyTokens(). */ + private HostInfo mHost; + + /** Object for fetching OAuth2 access tokens from third party authorization servers. */ + private ThirdPartyTokenFetcher mTokenFetcher; + + public SessionAuthenticator(Chromoting context, HostInfo host) { + mApplicationContext = context; + mHost = host; + } + + public void displayAuthenticationPrompt(boolean pairingSupported) { + AlertDialog.Builder pinPrompt = new AlertDialog.Builder(mApplicationContext); + pinPrompt.setTitle(mApplicationContext.getString(R.string.title_authenticate)); + pinPrompt.setMessage(mApplicationContext.getString(R.string.pin_message_android)); + pinPrompt.setIcon(android.R.drawable.ic_lock_lock); + + final View pinEntry = + mApplicationContext.getLayoutInflater().inflate(R.layout.pin_dialog, null); + pinPrompt.setView(pinEntry); + + final TextView pinTextView = (TextView) pinEntry.findViewById(R.id.pin_dialog_text); + final CheckBox pinCheckBox = (CheckBox) pinEntry.findViewById(R.id.pin_dialog_check); + + if (!pairingSupported) { + pinCheckBox.setChecked(false); + pinCheckBox.setVisibility(View.GONE); + } + + pinPrompt.setPositiveButton( + R.string.connect_button, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (JniInterface.isConnected()) { + JniInterface.handleAuthenticationResponse( + String.valueOf(pinTextView.getText()), + pinCheckBox.isChecked(), Build.MODEL); + } else { + String message = + mApplicationContext.getString(R.string.error_network_error); + Toast.makeText(mApplicationContext, message, Toast.LENGTH_LONG).show(); + } + } + }); + + pinPrompt.setNegativeButton( + R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + JniInterface.disconnectFromHost(); + } + }); + + final AlertDialog pinDialog = pinPrompt.create(); + + pinTextView.setOnEditorActionListener( + new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + // The user pressed enter on the keypad (equivalent to the connect button). + pinDialog.getButton(AlertDialog.BUTTON_POSITIVE).performClick(); + pinDialog.dismiss(); + return true; + } + }); + + pinDialog.setOnCancelListener( + new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + // The user backed out of the dialog (equivalent to the cancel button). + pinDialog.getButton(AlertDialog.BUTTON_NEGATIVE).performClick(); + } + }); + + pinDialog.show(); + } + + /** Saves newly-received pairing credentials to permanent storage. */ + public void commitPairingCredentials(String host, String id, String secret) { + // Empty |id| indicates that pairing needs to be removed. + if (id.isEmpty()) { + mApplicationContext.getPreferences(Activity.MODE_PRIVATE).edit() + .remove(host + "_id") + .remove(host + "_secret") + .apply(); + } else { + mApplicationContext.getPreferences(Activity.MODE_PRIVATE).edit() + .putString(host + "_id", id) + .putString(host + "_secret", secret) + .apply(); + } + } + + public void fetchThirdPartyToken(String tokenUrl, String clientId, String scope) { + assert mTokenFetcher == null; + + ThirdPartyTokenFetcher.Callback callback = new ThirdPartyTokenFetcher.Callback() { + @Override + public void onTokenFetched(String code, String accessToken) { + // The native client sends the OAuth authorization code to the host as the token so + // that the host can obtain the shared secret from the third party authorization + // server. + String token = code; + + // The native client uses the OAuth access token as the shared secret to + // authenticate itself with the host using spake. + String sharedSecret = accessToken; + + JniInterface.onThirdPartyTokenFetched(token, sharedSecret); + } + }; + mTokenFetcher = new ThirdPartyTokenFetcher(mApplicationContext, mHost.getTokenUrlPatterns(), + callback); + mTokenFetcher.fetchToken(tokenUrl, clientId, scope); + } + + public void onNewIntent(Intent intent) { + if (mTokenFetcher != null) { + if (mTokenFetcher.handleTokenFetched(intent)) { + mTokenFetcher = null; + } + } + } + + /** Returns the pairing ID for the given host, or an empty string if not set. */ + public String getPairingId(String hostId) { + SharedPreferences prefs = mApplicationContext.getPreferences(Activity.MODE_PRIVATE); + return prefs.getString(hostId + "_id", ""); + } + + /** Returns the pairing secret for the given host, or an empty string if not set. */ + public String getPairingSecret(String hostId) { + SharedPreferences prefs = mApplicationContext.getPreferences(Activity.MODE_PRIVATE); + return prefs.getString(hostId + "_secret", ""); + } +} diff --git a/remoting/android/java/src/org/chromium/chromoting/SessionConnector.java b/remoting/android/java/src/org/chromium/chromoting/SessionConnector.java index 20fad25..cb57a21 100644 --- a/remoting/android/java/src/org/chromium/chromoting/SessionConnector.java +++ b/remoting/android/java/src/org/chromium/chromoting/SessionConnector.java @@ -15,6 +15,7 @@ public class SessionConnector implements JniInterface.ConnectionListener, private JniInterface.ConnectionListener mConnectionCallback; private HostListLoader.Callback mHostListCallback; private HostListLoader mHostListLoader; + private SessionAuthenticator mAuthenticator; private String mAccountName; private String mAuthToken; @@ -43,11 +44,13 @@ public class SessionConnector implements JniInterface.ConnectionListener, } /** Initiates a connection to the host. */ - public void connectToHost(String accountName, String authToken, HostInfo host) { + public void connectToHost(String accountName, String authToken, HostInfo host, + SessionAuthenticator authenticator) { mAccountName = accountName; mAuthToken = authToken; mHostId = host.id; mHostJabberId = host.jabberId; + mAuthenticator = authenticator; if (hostIncomplete(host)) { // These keys might not be present in a newly-registered host, so treat this as a @@ -57,7 +60,7 @@ public class SessionConnector implements JniInterface.ConnectionListener, } JniInterface.connectToHost(accountName, authToken, host.jabberId, host.id, host.publicKey, - this); + this, mAuthenticator); } private static boolean hostIncomplete(HostInfo host) { @@ -108,7 +111,7 @@ public class SessionConnector implements JniInterface.ConnectionListener, // Reconnect to the host, but use the original callback directly, instead of this // wrapper object, so the host list is not loaded again. JniInterface.connectToHost(mAccountName, mAuthToken, foundHost.jabberId, - foundHost.id, foundHost.publicKey, mConnectionCallback); + foundHost.id, foundHost.publicKey, mConnectionCallback, mAuthenticator); } } diff --git a/remoting/android/java/src/org/chromium/chromoting/jni/JniInterface.java b/remoting/android/java/src/org/chromium/chromoting/jni/JniInterface.java index a4f2fc5..7057179 100644 --- a/remoting/android/java/src/org/chromium/chromoting/jni/JniInterface.java +++ b/remoting/android/java/src/org/chromium/chromoting/jni/JniInterface.java @@ -4,27 +4,17 @@ package org.chromium.chromoting.jni; -import android.app.Activity; -import android.app.AlertDialog; import android.content.Context; -import android.content.DialogInterface; -import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.Point; -import android.os.Build; import android.os.Looper; import android.util.Log; -import android.view.KeyEvent; -import android.view.View; -import android.widget.CheckBox; -import android.widget.TextView; -import android.widget.Toast; import org.chromium.base.CalledByNative; import org.chromium.base.JNINamespace; import org.chromium.chromoting.CapabilityManager; -import org.chromium.chromoting.Chromoting; import org.chromium.chromoting.R; +import org.chromium.chromoting.SessionAuthenticator; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -41,8 +31,8 @@ public class JniInterface { /** Whether the library has been loaded. Accessed on the UI thread. */ private static boolean sLoaded = false; - /** The application context. Accessed on the UI thread. */ - private static Activity sContext = null; + /** Used for authentication-related UX during connection. Accessed on the UI thread. */ + private static SessionAuthenticator sAuthenticator; /** Interface used for connection state notifications. */ public interface ConnectionListener { @@ -152,9 +142,7 @@ public class JniInterface { * context, but not reload the library. This is useful e.g. when the activity is closed and the * user later wants to return to the application. Called on the UI thread. */ - public static void loadLibrary(Activity context) { - sContext = context; - + public static void loadLibrary(Context context) { if (sLoaded) return; System.loadLibrary("remoting_client_jni"); @@ -173,15 +161,21 @@ public class JniInterface { public static native String nativeGetClientId(); public static native String nativeGetClientSecret(); + /** Returns whether the client is connected. */ + public static boolean isConnected() { + return sConnected; + } + /** Attempts to form a connection to the user-selected host. Called on the UI thread. */ public static void connectToHost(String username, String authToken, - String hostJid, String hostId, String hostPubkey, ConnectionListener listener) { + String hostJid, String hostId, String hostPubkey, ConnectionListener listener, + SessionAuthenticator authenticator) { disconnectFromHost(); sConnectionListener = listener; - SharedPreferences prefs = sContext.getPreferences(Activity.MODE_PRIVATE); + sAuthenticator = authenticator; nativeConnect(username, authToken, hostJid, hostId, hostPubkey, - prefs.getString(hostId + "_id", ""), prefs.getString(hostId + "_secret", ""), + sAuthenticator.getPairingId(hostId), sAuthenticator.getPairingSecret(hostId), sCapabilityManager.getLocalCapabilities()); sConnected = true; } @@ -238,69 +232,7 @@ public class JniInterface { /** Prompts the user to enter a PIN. Called on the UI thread. */ @CalledByNative private static void displayAuthenticationPrompt(boolean pairingSupported) { - AlertDialog.Builder pinPrompt = new AlertDialog.Builder(sContext); - pinPrompt.setTitle(sContext.getString(R.string.title_authenticate)); - pinPrompt.setMessage(sContext.getString(R.string.pin_message_android)); - pinPrompt.setIcon(android.R.drawable.ic_lock_lock); - - final View pinEntry = sContext.getLayoutInflater().inflate(R.layout.pin_dialog, null); - pinPrompt.setView(pinEntry); - - final TextView pinTextView = (TextView) pinEntry.findViewById(R.id.pin_dialog_text); - final CheckBox pinCheckBox = (CheckBox) pinEntry.findViewById(R.id.pin_dialog_check); - - if (!pairingSupported) { - pinCheckBox.setChecked(false); - pinCheckBox.setVisibility(View.GONE); - } - - pinPrompt.setPositiveButton( - R.string.connect_button, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Log.i("jniiface", "User provided a PIN code"); - if (sConnected) { - nativeAuthenticationResponse(String.valueOf(pinTextView.getText()), - pinCheckBox.isChecked(), Build.MODEL); - } else { - String message = sContext.getString(R.string.error_network_error); - Toast.makeText(sContext, message, Toast.LENGTH_LONG).show(); - } - } - }); - - pinPrompt.setNegativeButton( - R.string.cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Log.i("jniiface", "User canceled pin entry prompt"); - disconnectFromHost(); - } - }); - - final AlertDialog pinDialog = pinPrompt.create(); - - pinTextView.setOnEditorActionListener( - new TextView.OnEditorActionListener() { - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - // The user pressed enter on the keypad (equivalent to the connect button). - pinDialog.getButton(AlertDialog.BUTTON_POSITIVE).performClick(); - pinDialog.dismiss(); - return true; - } - }); - - pinDialog.setOnCancelListener( - new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - // The user backed out of the dialog (equivalent to the cancel button). - pinDialog.getButton(AlertDialog.BUTTON_NEGATIVE).performClick(); - } - }); - - pinDialog.show(); + sAuthenticator.displayAuthenticationPrompt(pairingSupported); } /** @@ -310,24 +242,20 @@ public class JniInterface { * @param deviceName The device name to appear in the pairing registry. Only used if createPair * is true. */ + public static void handleAuthenticationResponse(String pin, boolean createPair, + String deviceName) { + assert sConnected; + nativeAuthenticationResponse(pin, createPair, deviceName); + } + + /** Native implementation of handleAuthenticationResponse(). */ private static native void nativeAuthenticationResponse(String pin, boolean createPair, String deviceName); /** Saves newly-received pairing credentials to permanent storage. Called on the UI thread. */ @CalledByNative private static void commitPairingCredentials(String host, String id, String secret) { - // Empty |id| indicates that pairing needs to be removed. - if (id.isEmpty()) { - sContext.getPreferences(Activity.MODE_PRIVATE).edit() - .remove(host + "_id") - .remove(host + "_secret") - .apply(); - } else { - sContext.getPreferences(Activity.MODE_PRIVATE).edit() - .putString(host + "_id", id) - .putString(host + "_secret", secret) - .apply(); - } + sAuthenticator.commitPairingCredentials(host, id, secret); } /** @@ -498,8 +426,7 @@ public class JniInterface { /** Pops up a third party login page to fetch the token required for authentication. */ @CalledByNative public static void fetchThirdPartyToken(String tokenUrl, String clientId, String scope) { - Chromoting app = (Chromoting) sContext; - app.fetchThirdPartyToken(tokenUrl, clientId, scope); + sAuthenticator.fetchThirdPartyToken(tokenUrl, clientId, scope); } /** |