diff --git a/WordPress/build.gradle b/WordPress/build.gradle index e8cc51ab3ef5..9bb28e36cf68 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -87,6 +87,7 @@ dependencies { compile 'com.android.support:recyclerview-v7:23.1.1' compile 'com.android.support:design:23.1.1' compile 'com.google.android.gms:play-services-gcm:8.3.0' + compile 'com.google.android.gms:play-services-auth:8.3.0' compile 'com.github.chrisbanes.photoview:library:1.2.4' compile 'com.helpshift:android-helpshift-aar:4.4.0' compile 'de.greenrobot:eventbus:2.4.0' diff --git a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java index 344ca2ec555b..94deaea448b4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java @@ -270,7 +270,7 @@ public static void viewStatsSinglePostDetails(Context context, PostModel post) { public static void addSelfHostedSiteForResult(Activity activity) { Intent intent = new Intent(activity, SignInActivity.class); - intent.putExtra(SignInActivity.START_FRAGMENT_KEY, SignInActivity.ADD_SELF_HOSTED_BLOG); + intent.putExtra(SignInActivity.EXTRA_START_FRAGMENT, SignInActivity.ADD_SELF_HOSTED_BLOG); activity.startActivityForResult(intent, RequestCodes.ADD_ACCOUNT); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignInActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignInActivity.java index bf91585a8668..dc1191561d52 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignInActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignInActivity.java @@ -1,25 +1,34 @@ package org.wordpress.android.ui.accounts; -import android.app.Activity; import android.app.FragmentManager; import android.content.Intent; import android.os.Bundle; +import android.support.v4.app.FragmentActivity; import android.view.Window; +import com.google.android.gms.auth.api.credentials.Credential; + import org.wordpress.android.R; import org.wordpress.android.WordPress; +import org.wordpress.android.analytics.AnalyticsTracker; +import org.wordpress.android.analytics.AnalyticsTracker.Stat; import org.wordpress.android.models.Blog; import org.wordpress.android.ui.ActivityId; +import org.wordpress.android.util.AppLog; +import org.wordpress.android.util.AppLog.T; -public class SignInActivity extends Activity { +public class SignInActivity extends FragmentActivity { public static final int SIGN_IN_REQUEST = 1; public static final int REQUEST_CODE = 5000; public static final int ADD_SELF_HOSTED_BLOG = 2; public static final int SHOW_CERT_DETAILS = 4; - public static String START_FRAGMENT_KEY = "start-fragment"; - public static final String ARG_JETPACK_SITE_AUTH = "ARG_JETPACK_SITE_AUTH"; - public static final String ARG_JETPACK_MESSAGE_AUTH = "ARG_JETPACK_MESSAGE_AUTH"; - public static final String ARG_IS_AUTH_ERROR = "ARG_IS_AUTH_ERROR"; + public static final int SMART_LOCK_SAVE = 5; + public static final int SMART_LOCK_READ = 6; + + public static final String EXTRA_START_FRAGMENT = "start-fragment"; + public static final String EXTRA_JETPACK_SITE_AUTH = "EXTRA_JETPACK_SITE_AUTH"; + public static final String EXTRA_JETPACK_MESSAGE_AUTH = "EXTRA_JETPACK_MESSAGE_AUTH"; + public static final String EXTRA_IS_AUTH_ERROR = "EXTRA_IS_AUTH_ERROR"; private SignInFragment mSignInFragment; @@ -31,22 +40,20 @@ protected void onCreate(Bundle savedInstanceState) { FragmentManager fragmentManager = getFragmentManager(); mSignInFragment = (SignInFragment) fragmentManager.findFragmentById(R.id.sign_in_fragment); actionMode(getIntent().getExtras()); - ActivityId.trackLastActivity(ActivityId.LOGIN); } private void actionMode(Bundle extras) { int actionMode = SIGN_IN_REQUEST; if (extras != null) { - actionMode = extras.getInt(START_FRAGMENT_KEY, -1); - - if (extras.containsKey(ARG_JETPACK_SITE_AUTH)) { - Blog jetpackBlog = WordPress.getBlog(extras.getInt(ARG_JETPACK_SITE_AUTH)); + actionMode = extras.getInt(EXTRA_START_FRAGMENT, -1); + if (extras.containsKey(EXTRA_JETPACK_SITE_AUTH)) { + Blog jetpackBlog = WordPress.getBlog(extras.getInt(EXTRA_JETPACK_SITE_AUTH)); if (jetpackBlog != null) { - String customMessage = extras.getString(ARG_JETPACK_MESSAGE_AUTH, null); + String customMessage = extras.getString(EXTRA_JETPACK_MESSAGE_AUTH, null); mSignInFragment.setBlogAndCustomMessageForJetpackAuth(jetpackBlog, customMessage); } - } else if (extras.containsKey(ARG_IS_AUTH_ERROR)) { + } else if (extras.containsKey(EXTRA_IS_AUTH_ERROR)) { mSignInFragment.showAuthErrorMessage(); } } @@ -57,8 +64,7 @@ private void actionMode(Bundle extras) { default: break; } - - + mSignInFragment.smartLockAutoFill(); } @Override @@ -67,6 +73,21 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == SHOW_CERT_DETAILS) { mSignInFragment.askForSslTrust(); + } else if (requestCode == SMART_LOCK_SAVE) { + if (resultCode == RESULT_OK) { + AnalyticsTracker.track(Stat.LOGIN_AUTOFILL_CREDENTIALS_UPDATED); + AppLog.d(T.NUX, "Credentials saved"); + } else { + AppLog.d(T.NUX, "Credentials save cancelled"); + } + } else if (requestCode == SMART_LOCK_READ) { + if (resultCode == RESULT_OK) { + AppLog.d(T.NUX, "Credentials retrieved"); + Credential credential = data.getParcelableExtra(Credential.EXTRA_KEY); + mSignInFragment.onCredentialRetrieved(credential); + } else { + AppLog.e(T.NUX, "Credential read failed"); + } } else if (resultCode == RESULT_OK && data != null) { String username = data.getStringExtra("username"); String password = data.getStringExtra("password"); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignInFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignInFragment.java index fc017b60298f..c44d8e9c0f63 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignInFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignInFragment.java @@ -6,6 +6,7 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.IntentSender; import android.content.res.Configuration; import android.net.Uri; import android.os.AsyncTask; @@ -28,6 +29,17 @@ import android.widget.RelativeLayout; import android.widget.TextView; +import com.google.android.gms.auth.api.Auth; +import com.google.android.gms.auth.api.credentials.Credential; +import com.google.android.gms.auth.api.credentials.CredentialRequest; +import com.google.android.gms.auth.api.credentials.CredentialRequestResult; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.CommonStatusCodes; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; +import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.common.api.Status; import com.wordpress.rest.RestRequest; import org.json.JSONException; @@ -70,7 +82,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -public class SignInFragment extends AbstractFragment implements TextWatcher { +public class SignInFragment extends AbstractFragment implements TextWatcher, ConnectionCallbacks, + OnConnectionFailedListener { private static final String DOT_COM_BASE_URL = "https://wordpress.com"; private static final String FORGOT_PASSWORD_RELATIVE_URL = "/wp-login.php?action=lostpassword"; private static final int WPCOM_ERRONEOUS_LOGIN_THRESHOLD = 3; @@ -116,6 +129,8 @@ public class SignInFragment extends AbstractFragment implements TextWatcher { private String mHttpPassword; private Blog mJetpackBlog; + private GoogleApiClient mCredentialsClient; + public SignInFragment() { mEmailChecker = new EmailChecker(); } @@ -206,6 +221,7 @@ public void onClick(View v) { initPasswordVisibilityButton(rootView, mPasswordEditText); initInfoButtons(rootView); moveBottomButtons(); + initSmartLockForPasswords(); return rootView; } @@ -275,6 +291,66 @@ public void onClick(View v) { mInfoButtonSecondary.setOnClickListener(infoButtonListener); } + private void initSmartLockForPasswords() { + mCredentialsClient = new GoogleApiClient.Builder(getActivity()) + .addConnectionCallbacks(this) + .enableAutoManage((SignInActivity) getActivity(), this) + .addApi(Auth.CREDENTIALS_API) + .build(); + } + + public void smartLockAutoFill() { + CredentialRequest credentialRequest = new CredentialRequest.Builder() + .setSupportsPasswordLogin(true) + .build(); + Auth.CredentialsApi.request(mCredentialsClient, credentialRequest).setResultCallback( + new ResultCallback() { + @Override + public void onResult(CredentialRequestResult result) { + Status status = result.getStatus(); + if (status.isSuccess()) { + Credential credential = result.getCredential(); + onCredentialRetrieved(credential); + } else { + if (status.getStatusCode() == CommonStatusCodes.RESOLUTION_REQUIRED) { + try { + // Prompt the user to choose a saved credential + status.startResolutionForResult(getActivity(), SignInActivity.SMART_LOCK_READ); + } catch (IntentSender.SendIntentException e) { + AppLog.d(T.NUX, "SmartLock: Failed to send resolution for credential " + + "request"); + } + } else { + // The user must create an account or sign in manually. + AppLog.d(T.NUX, "SmartLock: Unsuccessful credential request."); + } + } + } + }); + } + + public void onCredentialRetrieved(Credential credential) { + AppLog.d(T.NUX, "Retrieved username from SmartLock: " + credential.getId()); + AnalyticsTracker.track(Stat.LOGIN_AUTOFILL_CREDENTIALS_FILLED); + mUsernameEditText.setText(credential.getId()); + mPasswordEditText.setText(credential.getPassword()); + } + + @Override + public void onConnectionFailed(ConnectionResult connectionResult) { + AppLog.d(T.NUX, "Connection result: " + connectionResult); + } + + @Override + public void onConnected(Bundle bundle) { + AppLog.d(T.NUX, "Google API client connected"); + } + + @Override + public void onConnectionSuspended(int i) { + AppLog.d(T.NUX, "Google API client connection suspended"); + } + private void setSecondaryButtonVisible(boolean visible) { mInfoButtonSecondary.setVisibility(visible ? View.VISIBLE : View.GONE); mInfoButton.setVisibility(visible ? View.GONE : View.VISIBLE); @@ -562,12 +638,43 @@ private boolean hasHardwareKeyboard() { return (getResources().getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS); } + private void saveCrendentialsInSmartLock() { + Credential credential = new Credential.Builder(mUsername).setPassword(mPassword).build(); + Auth.CredentialsApi.save(mCredentialsClient, credential).setResultCallback( + new ResultCallback() { + @Override + public void onResult(Status status) { + if (!status.isSuccess() && status.hasResolution()) { + try { + // This prompt the user to resolve the save request + status.startResolutionForResult(getActivity(), SignInActivity.SMART_LOCK_SAVE); + } catch (IntentSender.SendIntentException e) { + // Could not resolve the request + } + } + } + }); + } + + private void deleteCredentialsInSmartLock() { + Credential credential = new Credential.Builder(mUsername).setPassword(mPassword).build(); + Auth.CredentialsApi.delete(mCredentialsClient, credential).setResultCallback( + new ResultCallback() { + @Override + public void onResult(Status status) { + AppLog.i(T.NUX, status.isSuccess() ? "SmartLock: credentials deleted for username: " + mUsername + : "SmartLock: Credentials not deleted for username: " + mUsername ); + } + }); + } + private void signInAndFetchBlogListWPCom() { LoginWPCom login = new LoginWPCom(mUsername, mPassword, mTwoStepCode, mShouldSendTwoStepSMS, mJetpackBlog); login.execute(new LoginAbstract.Callback() { @Override public void onSuccess() { mShouldSendTwoStepSMS = false; + saveCrendentialsInSmartLock(); // Finish this activity if we've authenticated to a Jetpack site if (isJetpackAuth() && getActivity() != null) { @@ -584,6 +691,10 @@ public void onSuccess() { public void onError(int errorMessageId, boolean twoStepCodeRequired, boolean httpAuthRequired, boolean erroneousSslCertificate) { mFetchBlogListCallback.onError(errorMessageId, twoStepCodeRequired, httpAuthRequired, erroneousSslCertificate, ""); mShouldSendTwoStepSMS = false; + // Delete credentials only if login failed with an incorrect username/password error + if (errorMessageId == R.string.username_or_password_incorrect) { + deleteCredentialsInSmartLock(); + } } }); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsActivity.java index 4507e3aaacd0..b18182da3027 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsActivity.java @@ -519,9 +519,9 @@ private boolean updateTimeframeAndDateAndStartRefreshInFragment(FragmentManager private void startWPComLoginActivity() { mResultCode = RESULT_CANCELED; Intent signInIntent = new Intent(this, SignInActivity.class); - signInIntent.putExtra(SignInActivity.ARG_JETPACK_SITE_AUTH, mLocalBlogID); + signInIntent.putExtra(SignInActivity.EXTRA_JETPACK_SITE_AUTH, mLocalBlogID); signInIntent.putExtra( - SignInActivity.ARG_JETPACK_MESSAGE_AUTH, + SignInActivity.EXTRA_JETPACK_MESSAGE_AUTH, getString(R.string.stats_sign_in_jetpack_different_com_account) ); startActivityForResult(signInIntent, SignInActivity.REQUEST_CODE); diff --git a/WordPress/src/main/java/org/wordpress/android/util/AuthenticationDialogUtils.java b/WordPress/src/main/java/org/wordpress/android/util/AuthenticationDialogUtils.java index a029f546bfbe..5c952f0587ba 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/AuthenticationDialogUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/util/AuthenticationDialogUtils.java @@ -80,7 +80,7 @@ public static void showAuthErrorView(Activity activity, int titleResId, int mess // WP.com errors will show the sign in activity if (WordPress.getCurrentBlog() == null || (WordPress.getCurrentBlog() != null && WordPress.getCurrentBlog().isDotcomFlag())) { Intent signInIntent = new Intent(activity, SignInActivity.class); - signInIntent.putExtra(SignInActivity.ARG_IS_AUTH_ERROR, true); + signInIntent.putExtra(SignInActivity.EXTRA_IS_AUTH_ERROR, true); signInIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); activity.startActivityForResult(signInIntent, SignInActivity.REQUEST_CODE); return; diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java index eb7bf1e0fc5e..e0fe841562c4 100644 --- a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java +++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java @@ -118,6 +118,8 @@ public enum Stat { LOGIN_FAILED, LOGIN_FAILED_TO_GUESS_XMLRPC, LOGIN_INSERTED_INVALID_URL, + LOGIN_AUTOFILL_CREDENTIALS_FILLED, + LOGIN_AUTOFILL_CREDENTIALS_UPDATED, PUSH_AUTHENTICATION_APPROVED, PUSH_AUTHENTICATION_EXPIRED, PUSH_AUTHENTICATION_FAILED, diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanel.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanel.java index 1192b39724ca..7ad12caf9624 100644 --- a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanel.java +++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanel.java @@ -786,6 +786,18 @@ private AnalyticsTrackerMixpanelInstructionsForStat instructionsForStat( instructions = AnalyticsTrackerMixpanelInstructionsForStat. mixpanelInstructionsForEventName("Login - Failed To Guess XMLRPC"); break; + case LOGIN_INSERTED_INVALID_URL: + instructions = AnalyticsTrackerMixpanelInstructionsForStat. + mixpanelInstructionsForEventName("Login - Inserted Invalid URL"); + break; + case LOGIN_AUTOFILL_CREDENTIALS_FILLED: + instructions = AnalyticsTrackerMixpanelInstructionsForStat. + mixpanelInstructionsForEventName("Login - Auto Fill Credentials Filled"); + break; + case LOGIN_AUTOFILL_CREDENTIALS_UPDATED: + instructions = AnalyticsTrackerMixpanelInstructionsForStat. + mixpanelInstructionsForEventName("Login - Auto Fill Credentials Updated"); + break; case PUSH_AUTHENTICATION_APPROVED: instructions = AnalyticsTrackerMixpanelInstructionsForStat. mixpanelInstructionsForEventName("Push Authentication - Approved"); diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java index 49895df4ea87..fc89ec0eae1e 100644 --- a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java +++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java @@ -369,6 +369,12 @@ public void track(AnalyticsTracker.Stat stat, Map properties) { case LOGIN_INSERTED_INVALID_URL: eventName = "login_inserted_invalid_url"; break; + case LOGIN_AUTOFILL_CREDENTIALS_FILLED: + eventName = "login_autofill_credentials_filled"; + break; + case LOGIN_AUTOFILL_CREDENTIALS_UPDATED: + eventName = "login_autofill_credentials_updated"; + break; case PUSH_AUTHENTICATION_APPROVED: eventName = "push_authentication_approved"; break;