diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2115831..c60a17b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,28 +2,39 @@ + - + + + + + + android:theme="@style/FUTheme"> + + + android:resource="@xml/provider_paths" /> + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner/MainActivity.java b/app/src/main/java/de/sebse/fuplanner/MainActivity.java index 0a1ce48..f74b149 100644 --- a/app/src/main/java/de/sebse/fuplanner/MainActivity.java +++ b/app/src/main/java/de/sebse/fuplanner/MainActivity.java @@ -1,5 +1,8 @@ package de.sebse.fuplanner; +import android.accounts.AccountManager; +import android.accounts.AccountManagerCallback; +import android.accounts.AccountManagerFuture; import android.content.Intent; import android.os.Bundle; import android.view.Menu; @@ -46,6 +49,7 @@ import de.sebse.fuplanner.services.KVV.KVVListener; import de.sebse.fuplanner.services.KVV.types.LoginToken; import de.sebse.fuplanner.services.KVV.types.Modules; import de.sebse.fuplanner.services.News.NewsManager; +import de.sebse.fuplanner.services.newkvv.AccountGeneral; import de.sebse.fuplanner.tools.MainActivityListener; import de.sebse.fuplanner.tools.NewAsyncQueue; import de.sebse.fuplanner.tools.Preferences; @@ -93,10 +97,12 @@ public class MainActivity extends AppCompatActivity private boolean mOfflineBanner; private final NewAsyncQueue mQueue = new NewAsyncQueue(); private long mDoubleBackToExitPressedOnce = 0; + private AccountManager mAccountManager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + mAccountManager = AccountManager.get(this); int desiredPage = getDefaultFragmentAfterLogin(); String desiredData = ""; if (savedInstanceState != null) { @@ -158,6 +164,7 @@ public class MainActivity extends AppCompatActivity } else { mDoubleBackToExitPressedOnce = System.currentTimeMillis(); showToast(R.string.back_to_exit); + getTokenForAccountCreateIfNeeded(AccountGeneral.ACCOUNT_TYPE, AccountGeneral.AUTHTOKEN_TYPE_KVV); } } } @@ -552,6 +559,27 @@ public class MainActivity extends AppCompatActivity }); } + private void getTokenForAccountCreateIfNeeded(String accountType, String authTokenType) { + final AccountManagerFuture future = mAccountManager.getAuthTokenByFeatures(accountType, authTokenType, null, this, null, null, + new AccountManagerCallback() { + @Override + public void run(AccountManagerFuture future) { + Bundle bnd = null; + try { + bnd = future.getResult(); + final String authtoken = bnd.getString(AccountManager.KEY_AUTHTOKEN); + showToast(((authtoken != null) ? "SUCCESS!\ntoken: " + authtoken : "FAIL")); + log.d("udinic", "GetTokenForAccount Bundle is " + bnd); + + } catch (Exception e) { + e.printStackTrace(); + showToast(e.getMessage()); + } + } + } + , null); + } + diff --git a/app/src/main/java/de/sebse/fuplanner/services/newkvv/AccountGeneral.java b/app/src/main/java/de/sebse/fuplanner/services/newkvv/AccountGeneral.java new file mode 100644 index 0000000..2d98253 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner/services/newkvv/AccountGeneral.java @@ -0,0 +1,7 @@ +package de.sebse.fuplanner.services.newkvv; + +public class AccountGeneral { + public static final String ACCOUNT_TYPE = "de.sebse.fuplanner.fuauth"; + public static final String AUTHTOKEN_TYPE_KVV = "KVV"; + public static final String AUTHTOKEN_TYPE_BLACKBOARD = "Blackboard"; +} diff --git a/app/src/main/java/de/sebse/fuplanner/services/newkvv/FUAuthenticator.java b/app/src/main/java/de/sebse/fuplanner/services/newkvv/FUAuthenticator.java new file mode 100644 index 0000000..cd4ab43 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner/services/newkvv/FUAuthenticator.java @@ -0,0 +1,104 @@ +package de.sebse.fuplanner.services.newkvv; + +import android.accounts.AbstractAccountAuthenticator; +import android.accounts.Account; +import android.accounts.AccountAuthenticatorResponse; +import android.accounts.AccountManager; +import android.accounts.NetworkErrorException; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.text.TextUtils; + +import java.util.concurrent.ExecutionException; + +public class FUAuthenticator extends AbstractAccountAuthenticator { + + private final Context mContext; + + public FUAuthenticator(Context context) { + super(context); + this.mContext = context; + } + + @Override + public Bundle editProperties(AccountAuthenticatorResponse accountAuthenticatorResponse, String s) { + return null; + } + + @Override + public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException { + final Intent intent = new Intent(mContext, FUAuthenticatorActivity.class); + intent.putExtra(FUAuthenticatorActivity.ARG_ACCOUNT_TYPE, accountType); + intent.putExtra(FUAuthenticatorActivity.ARG_AUTH_TYPE, authTokenType); + intent.putExtra(FUAuthenticatorActivity.ARG_IS_ADDING_NEW_ACCOUNT, true); + intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); + final Bundle bundle = new Bundle(); + bundle.putParcelable(AccountManager.KEY_INTENT, intent); + return bundle; + } + + @Override + public Bundle confirmCredentials(AccountAuthenticatorResponse accountAuthenticatorResponse, Account account, Bundle bundle) throws NetworkErrorException { + return null; + } + + @Override + public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException { + + // Extract the username and password from the Account Manager, and ask + // the server for an appropriate AuthToken. + final AccountManager am = AccountManager.get(mContext); + + String authToken = am.peekAuthToken(account, authTokenType); + + // Lets give another try to authenticate the user + if (TextUtils.isEmpty(authToken)) { + final String password = am.getPassword(account); + if (password != null) { + try { + authToken = new UserLoginTask(account.name, password, authTokenType, null).execute((Void) null).get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + } + + // If we get an authToken - we return it + if (!TextUtils.isEmpty(authToken)) { + final Bundle result = new Bundle(); + result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); + result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); + result.putString(AccountManager.KEY_AUTHTOKEN, authToken); + return result; + } + + // If we get here, then we couldn't access the user's password - so we + // need to re-prompt them for their credentials. We do that by creating + // an intent to display our AuthenticatorActivity. + final Intent intent = new Intent(mContext, FUAuthenticatorActivity.class); + intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); + intent.putExtra(FUAuthenticatorActivity.ARG_ACCOUNT_TYPE, account.type); + intent.putExtra(FUAuthenticatorActivity.ARG_AUTH_TYPE, authTokenType); + final Bundle bundle = new Bundle(); + bundle.putParcelable(AccountManager.KEY_INTENT, intent); + return bundle; + } + + @Override + public String getAuthTokenLabel(String s) { + return null; + } + + @Override + public Bundle updateCredentials(AccountAuthenticatorResponse accountAuthenticatorResponse, Account account, String s, Bundle bundle) throws NetworkErrorException { + return null; + } + + @Override + public Bundle hasFeatures(AccountAuthenticatorResponse accountAuthenticatorResponse, Account account, String[] strings) throws NetworkErrorException { + return null; + } +} diff --git a/app/src/main/java/de/sebse/fuplanner/services/newkvv/FUAuthenticatorActivity.java b/app/src/main/java/de/sebse/fuplanner/services/newkvv/FUAuthenticatorActivity.java new file mode 100644 index 0000000..339f03a --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner/services/newkvv/FUAuthenticatorActivity.java @@ -0,0 +1,329 @@ +package de.sebse.fuplanner.services.newkvv; + +import android.accounts.Account; +import android.accounts.AccountAuthenticatorActivity; +import android.accounts.AccountManager; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.annotation.TargetApi; +import android.content.Intent; +import android.content.pm.PackageManager; + +import androidx.annotation.NonNull; + +import android.app.LoaderManager.LoaderCallbacks; + +import android.content.CursorLoader; +import android.content.Loader; +import android.database.Cursor; +import android.net.Uri; + +import android.os.Build; +import android.os.Bundle; +import android.provider.ContactsContract; +import android.text.TextUtils; +import android.view.KeyEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.inputmethod.EditorInfo; +import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +import de.sebse.fuplanner.R; + +import static de.sebse.fuplanner.services.newkvv.UserLoginTask.PARAM_USER_PASS; + +/** + * A login screen that offers login via email/password. + */ +public class FUAuthenticatorActivity extends AccountAuthenticatorActivity implements LoaderCallbacks { + + /** + * Id to identity READ_CONTACTS permission request. + */ + private static final int REQUEST_READ_CONTACTS = 0; + + /** + * Keep track of the login task to ensure we can cancel it if requested. + */ + UserLoginTask mAuthTask = null; + + // UI references. + private AutoCompleteTextView mEmailView; + EditText mPasswordView; + private View mProgressView; + private View mLoginFormView; + + + + public static final String ARG_ACCOUNT_TYPE = "ARG_ACCOUNT_TYPE"; + public static final String ARG_AUTH_TYPE = "ARG_AUTH_TYPE"; + public static final String ARG_IS_ADDING_NEW_ACCOUNT = "ARG_IS_ADDING_NEW_ACCOUNT"; + private String mAccountType; + private String mAuthTokenType; + private boolean mIsAddingNewAccount; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mAccountType = getIntent().getStringExtra(ARG_ACCOUNT_TYPE); + mAuthTokenType = getIntent().getStringExtra(ARG_AUTH_TYPE); + mIsAddingNewAccount = getIntent().getBooleanExtra(ARG_IS_ADDING_NEW_ACCOUNT, false); + + setContentView(R.layout.activity_fu_authenticator); + // Set up the login form. + mEmailView = (AutoCompleteTextView) findViewById(R.id.email); + populateAutoComplete(); + + mPasswordView = (EditText) findViewById(R.id.password); + mPasswordView.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) { + if (id == EditorInfo.IME_ACTION_DONE || id == EditorInfo.IME_NULL) { + attemptLogin(); + return true; + } + return false; + } + }); + + Button mEmailSignInButton = (Button) findViewById(R.id.email_sign_in_button); + mEmailSignInButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + attemptLogin(); + } + }); + + mLoginFormView = findViewById(R.id.login_form); + mProgressView = findViewById(R.id.login_progress); + } + + private void populateAutoComplete() { + if (!mayRequestContacts()) { + return; + } + + getLoaderManager().initLoader(0, null, this); + } + + private boolean mayRequestContacts() { + /*if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return true; + } + if (checkSelfPermission(READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) { + return true; + } + if (shouldShowRequestPermissionRationale(READ_CONTACTS)) { + Snackbar.make(mEmailView, R.string.permission_rationale, Snackbar.LENGTH_INDEFINITE) + .setAction(android.R.string.ok, new View.OnClickListener() { + @Override + @TargetApi(Build.VERSION_CODES.M) + public void onClick(View v) { + requestPermissions(new String[]{READ_CONTACTS}, REQUEST_READ_CONTACTS); + } + }); + } else { + requestPermissions(new String[]{READ_CONTACTS}, REQUEST_READ_CONTACTS); + }*/ + return false; + } + + /** + * Callback received when a permissions request has been completed. + */ + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + if (requestCode == REQUEST_READ_CONTACTS) { + if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + populateAutoComplete(); + } + } + } + + + /** + * Attempts to sign in or register the account specified by the login form. + * If there are form errors (invalid email, missing fields, etc.), the + * errors are presented and no actual login attempt is made. + */ + private void attemptLogin() { + if (mAuthTask != null) { + return; + } + + // Reset errors. + mEmailView.setError(null); + mPasswordView.setError(null); + + // Store values at the time of the login attempt. + String email = mEmailView.getText().toString(); + String password = mPasswordView.getText().toString(); + + boolean cancel = false; + View focusView = null; + + // Check for a valid password, if the user entered one. + if (!TextUtils.isEmpty(password) && !isPasswordValid(password)) { + mPasswordView.setError(getString(R.string.error_invalid_password)); + focusView = mPasswordView; + cancel = true; + } + + // Check for a valid email address. + if (TextUtils.isEmpty(email)) { + mEmailView.setError(getString(R.string.error_field_required)); + focusView = mEmailView; + cancel = true; + } else if (!isEmailValid(email)) { + mEmailView.setError(getString(R.string.error_invalid_email)); + focusView = mEmailView; + cancel = true; + } + + if (cancel) { + // There was an error; don't attempt login and focus the first + // form field with an error. + focusView.requestFocus(); + } else { + // Show a progress spinner, and kick off a background task to + // perform the user login attempt. + showProgress(true); + mAuthTask = new UserLoginTask(email, password, mAuthTokenType, this); + mAuthTask.execute((Void) null); + } + } + + private boolean isEmailValid(String email) { + //TODO: Replace this with your own logic + return email.contains("@"); + } + + private boolean isPasswordValid(String password) { + //TODO: Replace this with your own logic + return password.length() > 4; + } + + /** + * Shows the progress UI and hides the login form. + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2) + void showProgress(final boolean show) { + // On Honeycomb MR2 we have the ViewPropertyAnimator APIs, which allow + // for very easy animations. If available, use these APIs to fade-in + // the progress spinner. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) { + int shortAnimTime = getResources().getInteger(android.R.integer.config_shortAnimTime); + + mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); + mLoginFormView.animate().setDuration(shortAnimTime).alpha( + show ? 0 : 1).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); + } + }); + + mProgressView.setVisibility(show ? View.VISIBLE : View.GONE); + mProgressView.animate().setDuration(shortAnimTime).alpha( + show ? 1 : 0).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mProgressView.setVisibility(show ? View.VISIBLE : View.GONE); + } + }); + } else { + // The ViewPropertyAnimator APIs are not available, so simply show + // and hide the relevant UI components. + mProgressView.setVisibility(show ? View.VISIBLE : View.GONE); + mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); + } + } + + @Override + public Loader onCreateLoader(int i, Bundle bundle) { + return new CursorLoader(this, + // Retrieve data rows for the device user's 'profile' contact. + Uri.withAppendedPath(ContactsContract.Profile.CONTENT_URI, + ContactsContract.Contacts.Data.CONTENT_DIRECTORY), ProfileQuery.PROJECTION, + + // Select only email addresses. + ContactsContract.Contacts.Data.MIMETYPE + + " = ?", new String[]{ContactsContract.CommonDataKinds.Email + .CONTENT_ITEM_TYPE}, + + // Show primary email addresses first. Note that there won't be + // a primary email address if the user hasn't specified one. + ContactsContract.Contacts.Data.IS_PRIMARY + " DESC"); + } + + @Override + public void onLoadFinished(Loader cursorLoader, Cursor cursor) { + List emails = new ArrayList<>(); + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + emails.add(cursor.getString(ProfileQuery.ADDRESS)); + cursor.moveToNext(); + } + + addEmailsToAutoComplete(emails); + } + + @Override + public void onLoaderReset(Loader cursorLoader) { + + } + + private void addEmailsToAutoComplete(List emailAddressCollection) { + //Create adapter to tell the AutoCompleteTextView what to show in its dropdown list. + ArrayAdapter adapter = + new ArrayAdapter<>(FUAuthenticatorActivity.this, + android.R.layout.simple_dropdown_item_1line, emailAddressCollection); + + mEmailView.setAdapter(adapter); + } + + + private interface ProfileQuery { + String[] PROJECTION = { + ContactsContract.CommonDataKinds.Email.ADDRESS, + ContactsContract.CommonDataKinds.Email.IS_PRIMARY, + }; + + int ADDRESS = 0; + int IS_PRIMARY = 1; + } + + void finishLogin(Intent intent) { + String accountName = intent.getStringExtra(AccountManager.KEY_ACCOUNT_NAME); + String accountPassword = intent.getStringExtra(PARAM_USER_PASS); + final Account account = new Account(accountName, intent.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE)); + final AccountManager mAccountManager = AccountManager.get(this); + + if (getIntent().getBooleanExtra(ARG_IS_ADDING_NEW_ACCOUNT, false)) { + String authtoken = intent.getStringExtra(AccountManager.KEY_AUTHTOKEN); + String authtokenType = mAuthTokenType; + + // Creating the account on the device and setting the auth token we got + // (Not setting the auth token will cause another call to the server to authenticate the user) + mAccountManager.addAccountExplicitly(account, accountPassword, null); + mAccountManager.setAuthToken(account, authtokenType, authtoken); + } else { + mAccountManager.setPassword(account, accountPassword); + } + + setAccountAuthenticatorResult(intent.getExtras()); + setResult(RESULT_OK, intent); + finish(); + } +} + diff --git a/app/src/main/java/de/sebse/fuplanner/services/newkvv/FUAuthenticatorService.java b/app/src/main/java/de/sebse/fuplanner/services/newkvv/FUAuthenticatorService.java new file mode 100644 index 0000000..a6f1516 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner/services/newkvv/FUAuthenticatorService.java @@ -0,0 +1,13 @@ +package de.sebse.fuplanner.services.newkvv; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; + +public class FUAuthenticatorService extends Service { + @Override + public IBinder onBind(Intent intent) { + FUAuthenticator authenticator = new FUAuthenticator(this); + return authenticator.getIBinder(); + } +} diff --git a/app/src/main/java/de/sebse/fuplanner/services/newkvv/UserLoginTask.java b/app/src/main/java/de/sebse/fuplanner/services/newkvv/UserLoginTask.java new file mode 100644 index 0000000..8aa3c65 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner/services/newkvv/UserLoginTask.java @@ -0,0 +1,92 @@ +package de.sebse.fuplanner.services.newkvv; + +import android.accounts.AccountManager; +import android.annotation.SuppressLint; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Build; + +import androidx.annotation.Nullable; +import de.sebse.fuplanner.R; + + +/** + * Represents an asynchronous login/registration task used to authenticate + * the user. + */ +public class UserLoginTask extends AsyncTask { + + /** + * A dummy authentication store containing known user names and passwords. + * TODO: remove after connecting to a real authentication system. + */ + private static final String[] DUMMY_CREDENTIALS = new String[]{ + "foo@example.com:hello", "bar@example.com:world" + }; + static final String PARAM_USER_PASS = "PARAM_USER_PASS"; + + private final String mEmail; + private final String mPassword; + private String mTokenType; + @SuppressLint("StaticFieldLeak") + @Nullable + private FUAuthenticatorActivity mActivity; + + UserLoginTask(String email, String password, String tokenType, @Nullable FUAuthenticatorActivity activity) { + mEmail = email; + mPassword = password; + mTokenType = tokenType; + mActivity = activity; + } + + @Override + protected String doInBackground(Void... params) { + // TODO: attempt authentication against a network service. + + try { + // Simulate network access. + Thread.sleep(2000); + } catch (InterruptedException e) { + return null; + } + + for (String credential : DUMMY_CREDENTIALS) { + String[] pieces = credential.split(":"); + if (pieces[0].equals(mEmail)) { + // Account exists, return true if the password matches. + return pieces[1].equals(mPassword) ? "auth token here" : null; + } + } + + // TODO: register the new account here. + return null; + } + + @Override + protected void onPostExecute(final String success) { + if (mActivity == null || mActivity.isFinishing()) + return; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && mActivity.isDestroyed()) + return; + mActivity.mAuthTask = null; + mActivity.showProgress(false); + + if (success != null) { + final Intent res = new Intent(); + res.putExtra(AccountManager.KEY_ACCOUNT_NAME, mEmail); + res.putExtra(AccountManager.KEY_ACCOUNT_TYPE, AccountGeneral.ACCOUNT_TYPE); + res.putExtra(AccountManager.KEY_AUTHTOKEN, success); + res.putExtra(PARAM_USER_PASS, mPassword); + mActivity.finishLogin(res); + } else { + mActivity.mPasswordView.setError(mActivity.getString(R.string.error_incorrect_password)); + mActivity.mPasswordView.requestFocus(); + } + } + + @Override + protected void onCancelled() { + mActivity.mAuthTask = null; + mActivity.showProgress(false); + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_fu_authenticator.xml b/app/src/main/res/layout/activity_fu_authenticator.xml new file mode 100644 index 0000000..3e432df --- /dev/null +++ b/app/src/main/res/layout/activity_fu_authenticator.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + +