package net.mvla.mvhs.aeries; import android.annotation.TargetApi; import android.app.Activity; import android.app.Fragment; import android.app.PendingIntent; import android.content.Intent; import android.content.IntentSender; import android.graphics.Bitmap; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.support.annotation.Nullable; import android.support.design.widget.Snackbar; import android.support.v7.app.AppCompatActivity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.webkit.JavascriptInterface; import android.webkit.WebChromeClient; import android.webkit.WebResourceError; import android.webkit.WebResourceRequest; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.Button; import android.widget.EditText; 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.CredentialPickerConfig; import com.google.android.gms.auth.api.credentials.CredentialRequest; import com.google.android.gms.auth.api.credentials.HintRequest; import com.google.android.gms.common.api.CommonStatusCodes; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.ResolvingResultCallbacks; import com.google.android.gms.common.api.Status; import net.mvla.mvhs.R; import net.mvla.mvhs.Utils; public class AeriesFragment extends Fragment { public static final String SAVE_STATE_CREDENTIAL = "save_state_credential"; public static final String SAVE_STATE_RESOLVING = "save_state_resolving"; public static final int RC_SAVE = 0; public static final int RC_READ = 1; public static final int RC_HINT = 2; private Credential mCurrentCredential; private Credential mCredentialToSave; private EditText mUsernameEditText; private EditText mPasswordEditText; private WebView mWebView; private Handler mHandler; private boolean mLoggedIn; private GoogleApiClient mGoogleApiClient; private View mLoginLayout; private Button mLoginButton; private Snackbar mErrorSnackbar; private Snackbar mSavingSnackbar; private boolean mIsResolving; private boolean mAttemptRequestCredentials = true; private boolean mNeedToRequestCredentials = true; @Override public void onStart() { super.onStart(); } @Override public void onStop() { super.onStop(); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(SAVE_STATE_CREDENTIAL, mCurrentCredential); outState.putBoolean(SAVE_STATE_RESOLVING, mIsResolving); mWebView.saveState(outState); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mHandler = new Handler(getActivity().getMainLooper()); if (savedInstanceState != null) { mIsResolving = savedInstanceState.getBoolean(SAVE_STATE_RESOLVING); mCurrentCredential = savedInstanceState.getParcelable(SAVE_STATE_CREDENTIAL); } mGoogleApiClient = new GoogleApiClient.Builder(getActivity()) .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() { @Override public void onConnected(Bundle bundle) { saveCredentialIfConnected(mCredentialToSave); if (mNeedToRequestCredentials) { attemptRequestCredentials(); } } @Override public void onConnectionSuspended(int i) { } }) .enableAutoManage(((AppCompatActivity) getActivity()), connectionResult -> { showSnackbarMessage("Error"); }) .addApi(Auth.CREDENTIALS_API) .build(); } @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_aeries, container, false); mWebView = (WebView) view.findViewById(R.id.fragment_aeries_webview); mWebView.getSettings().setJavaScriptEnabled(true); mWebView.setWebViewClient(new AeriesWebViewClient()); mWebView.setWebChromeClient(new WebChromeClient() { @Override public void onProgressChanged(WebView view, int newProgress) { super.onProgressChanged(view, newProgress); if (getActivity() != null) { ((AeriesActivity) getActivity()).setProgressBarProgress(newProgress); } } }); mWebView.addJavascriptInterface(this, "android"); mLoginLayout = view.findViewById(R.id.fragment_aeries_login_linear); mUsernameEditText = (EditText) view.findViewById(R.id.fragment_aeries_login_username); mPasswordEditText = (EditText) view.findViewById(R.id.fragment_aeries_login_password); mPasswordEditText.setOnEditorActionListener((v, actionId, event) -> { if (actionId == EditorInfo.IME_ACTION_SEARCH || actionId == EditorInfo.IME_ACTION_DONE || event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { mLoginButton.callOnClick(); return true; } return false; }); mLoginButton = (Button) view.findViewById(R.id.fragment_aeries_login_button); mLoginButton.setOnClickListener(v -> { mAttemptRequestCredentials = false; login(mUsernameEditText.getText().toString(), mPasswordEditText.getText().toString()); Utils.hideSoftKeyBoard(getActivity()); }); if (savedInstanceState == null) { mWebView.loadUrl("https://mvla.asp.aeries.net/student/LoginParent.aspx"); mAttemptRequestCredentials = true; } else { mCurrentCredential = savedInstanceState.getParcelable(SAVE_STATE_CREDENTIAL); mWebView.restoreState(savedInstanceState); } return view; } private void login(String username, String password) { Utils.executeJavascript(mWebView, "document.getElementById(\"portalAccountUsername\").value=\"" + username + "\""); Utils.executeJavascript(mWebView, "document.getElementById(\"portalAccountPassword\").value=\"" + password + "\""); Utils.executeJavascript(mWebView, "document.getElementById(\"LoginButton\").click()"); } /** * Returns whether took care of it */ public boolean onBackPressed() { if (!mLoggedIn || !mWebView.canGoBack()) { return false; } else { mWebView.goBack(); return true; } } private void onCredentialsSaved(Credential credential) { mCurrentCredential = credential; showSnackbarMessage(getString(R.string.credentials_saved)); } private void onCredentialsSaveFailed() { showSnackbarMessage(getString(R.string.save_failed)); } private void showSnackbarMessage(String string) { Snackbar.make(mWebView, string, Snackbar.LENGTH_SHORT).show(); } /** * Request option 1: https://developers.google.com/identity/smartlock-passwords/android/overview */ private void attemptRequestCredentials() { if (mIsResolving) { return; } mNeedToRequestCredentials = true; mLoginButton.setText(getString(R.string.retrieving_credentials)); CredentialRequest request = new CredentialRequest.Builder() .setSupportsPasswordLogin(true) .build(); if (mGoogleApiClient.isConnected()) { Auth.CredentialsApi.request(mGoogleApiClient, request).setResultCallback( credentialRequestResult -> { finishAllLoading(); final Status status = credentialRequestResult.getStatus(); if (status.isSuccess()) { // Single credential and auto sign-in enabled. //Request 2 processRetrievedCredential(credentialRequestResult.getCredential(), false); } else if (status.getStatusCode() == CommonStatusCodes.RESOLUTION_REQUIRED) { // Multiple credentials - pick one //Request 3 resolveResult(status, RC_READ); } else if (status.getStatusCode() == CommonStatusCodes.SIGN_IN_REQUIRED) { loadHints(); } }); mNeedToRequestCredentials = false; } } private void loadHints() { HintRequest hintRequest = new HintRequest.Builder() .setHintPickerConfig(new CredentialPickerConfig.Builder() .setShowCancelButton(true) .setShowAddAccountButton(true) .build()) .setEmailAddressIdentifierSupported(true) .build(); PendingIntent intent = Auth.CredentialsApi.getHintPickerIntent(mGoogleApiClient, hintRequest); try { getActivity().startIntentSenderForResult(intent.getIntentSender(), RC_HINT, null, 0, 0, 0); mIsResolving = true; } catch (IntentSender.SendIntentException e) { //Could not start hint picker Intent mIsResolving = false; } } private void showIndeterminateProgressBar() { if (getActivity() != null) { ((AeriesActivity) getActivity()).showIndeterminateProgressBar(); } } private void finishAllLoading() { if (!isAdded()) { return; } mHandler.post(() -> { if (getActivity() != null) { ((AeriesActivity) getActivity()).hideIndeterminateProgressBar(); } if (mSavingSnackbar != null) { mSavingSnackbar.dismiss(); } mLoginButton.setEnabled(true); mLoginButton.setText(getString(R.string.login)); }); } private void resolveResult(Status status, int requestCode) { //noinspection StatementWithEmptyBody if (status != null && status.hasResolution()) { try { // -> onActivityResult status.startResolutionForResult(getActivity(), requestCode); } catch (IntentSender.SendIntentException e) { //STATUS: Failed to send resolution. finishAllLoading(); } } else { // The user must create an account or sign in manually. //STATUS: Unsuccessful credential request had no resolution. finishAllLoading(); } } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); finishAllLoading(); switch (requestCode) { case RC_HINT: case RC_READ: //Request 3/4 if (resultCode == Activity.RESULT_OK) { boolean isHint = (requestCode == RC_HINT); Credential credential = data.getParcelableExtra(Credential.EXTRA_KEY); processRetrievedCredential(credential, isHint); } /*else { //Credential Read: NOT OK }*/ mIsResolving = false; break; case RC_SAVE: if (resultCode == Activity.RESULT_OK) { onCredentialsSaved(mCredentialToSave); mCredentialToSave = null; } else { onCredentialsSaveFailed(); } mIsResolving = false; break; } } private void processRetrievedCredential(Credential credential, boolean isHint) { if (!isHint) { //Request 2/3 mUsernameEditText.setText(credential.getId()); mPasswordEditText.setText(credential.getPassword()); login(credential.getId(), credential.getPassword()); } else { //Request 4 mUsernameEditText.setText(credential.getId()); } } private void attemptSaveCredential() { if (!mUsernameEditText.getText().toString().isEmpty() && !mPasswordEditText.getText().toString().isEmpty()) { final Credential credential = new Credential.Builder(mUsernameEditText.getText().toString()) .setPassword(mPasswordEditText.getText().toString()) .build(); saveCredentialIfConnected(credential); } } private void saveCredentialIfConnected(Credential credential) { //Username and password fields are not empty if (credential == null) { return; } if (mCurrentCredential == null || mCurrentCredential.describeContents() != credential.describeContents()) { mSavingSnackbar = Snackbar.make(mWebView, R.string.saving_credentials, Snackbar.LENGTH_INDEFINITE); mSavingSnackbar.show(); } mCredentialToSave = credential; if (mGoogleApiClient.isConnected()) { Auth.CredentialsApi.save(mGoogleApiClient, credential).setResultCallback( new ResolvingResultCallbacks<Status>(getActivity(), RC_SAVE) { @Override public void onSuccess(Status status) { // Credentials were saved AeriesFragment.this.onCredentialsSaved(mCredentialToSave); AeriesFragment.this.finishAllLoading(); mCredentialToSave = null; } @Override public void onUnresolvableFailure(Status status) { AeriesFragment.this.finishAllLoading(); AeriesFragment.this.onCredentialsSaveFailed(); mCredentialToSave = null; } } ); } } @JavascriptInterface public void onFind(String value) { if (!value.equalsIgnoreCase("null")) { Snackbar.make(mWebView, R.string.incorrect_login, Snackbar.LENGTH_LONG).show(); } } private class AeriesWebViewClient extends WebViewClient { @SuppressWarnings("deprecation") @Override public void onReceivedError(final WebView view, int errorCode, String description, final String failingUrl) { super.onReceivedError(view, errorCode, description, failingUrl); onError(view, errorCode, description, failingUrl); } @TargetApi(Build.VERSION_CODES.M) @Override public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { super.onReceivedError(view, request, error); onError(view, error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); } private void onError(WebView view, int errorCode, String description, String failingUrl) { if (view != null && view.getContext() != null) { mErrorSnackbar = Snackbar.make(view, "Error " + errorCode + ": " + description, Snackbar.LENGTH_INDEFINITE) .setAction("Try Again", v -> { mErrorSnackbar.dismiss(); view.loadUrl(failingUrl); }); mErrorSnackbar.show(); finishAllLoading(); mLoginButton.setEnabled(false); mLoginButton.setText(R.string.error); } } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (url.startsWith("https://mvla.asp.aeries.net")) { return false; } else { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(url)); startActivity(intent); return true; } } @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); if (!isAdded()) { return; } //Login page if (url.equalsIgnoreCase("https://mvla.asp.aeries.net/student/LoginParent.aspx")) { if (mAttemptRequestCredentials) { showIndeterminateProgressBar(); attemptRequestCredentials(); mAttemptRequestCredentials = false; } else { finishAllLoading(); } for (String error : new String[]{"errorMessage_password", "errorMessage_username"}) { Utils.executeJavascript(mWebView, "android.onFind(document.getElementById(\"" + error + "\").innerText)"); } } } @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { super.onPageStarted(view, url, favicon); if (!isAdded()) { return; } if (url.equalsIgnoreCase("https://mvla.asp.aeries.net/student/LoginParent.aspx")) { //Login page mLoggedIn = false; mWebView.setFocusable(false); mLoginButton.setText(getString(R.string.loading)); mLoginButton.setEnabled(false); mLoginLayout.setVisibility(View.VISIBLE); } else if (url.equalsIgnoreCase("https://mvla.asp.aeries.net/student/m/loginparent.html")) { //Logged in mLoggedIn = true; mLoginLayout.setVisibility(View.GONE); mWebView.setFocusable(true); //If fields are not empty attemptSaveCredential(); } } } }