diff --git a/.idea/libraries/ComAndroidSupportAppcompatV71900_aar.xml b/.idea/libraries/ComAndroidSupportAppcompatV71900_aar.xml index 42e4e10..6dcf38c 100644 --- a/.idea/libraries/ComAndroidSupportAppcompatV71900_aar.xml +++ b/.idea/libraries/ComAndroidSupportAppcompatV71900_aar.xml @@ -1,6 +1,9 @@ - + + + + diff --git a/.idea/libraries/ComAndroidSupportGridlayoutV71900_aar.xml b/.idea/libraries/ComAndroidSupportGridlayoutV71900_aar.xml index 9ba6c15..73dd985 100644 --- a/.idea/libraries/ComAndroidSupportGridlayoutV71900_aar.xml +++ b/.idea/libraries/ComAndroidSupportGridlayoutV71900_aar.xml @@ -1,6 +1,9 @@ - + + + + diff --git a/.idea/libraries/support_v4_19_0_0.xml b/.idea/libraries/support_v4_19_0_0.xml index 0279a24..4b64197 100644 --- a/.idea/libraries/support_v4_19_0_0.xml +++ b/.idea/libraries/support_v4_19_0_0.xml @@ -2,6 +2,7 @@ + diff --git a/katana/build.gradle b/katana/build.gradle index 5a7369d..d1bc027 100644 --- a/katana/build.gradle +++ b/katana/build.gradle @@ -33,5 +33,5 @@ android { dependencies { compile 'com.android.support:gridlayout-v7:19.0.0' compile 'com.android.support:appcompat-v7:+' - compile 'com.android.support:support-v4:+' + compile 'com.android.support:support-v4:18.0.0' } diff --git a/katana/src/main/AndroidManifest.xml b/katana/src/main/AndroidManifest.xml index 4130157..1af4e51 100644 --- a/katana/src/main/AndroidManifest.xml +++ b/katana/src/main/AndroidManifest.xml @@ -29,9 +29,24 @@ + android:label="@string/title_activity_tcdsingle_viewer" + android:parentActivityName="org.techniche.technothlon.katana.MainActivity" > + + + + + + + + + diff --git a/katana/src/main/java/org/techniche/technothlon/katana/LoginActivity.java b/katana/src/main/java/org/techniche/technothlon/katana/LoginActivity.java new file mode 100644 index 0000000..cb42a64 --- /dev/null +++ b/katana/src/main/java/org/techniche/technothlon/katana/LoginActivity.java @@ -0,0 +1,256 @@ +package org.techniche.technothlon.katana; + +import android.accounts.AccountManager; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.annotation.TargetApi; +import android.app.Activity; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; +import android.widget.TextView; + +/** + * Activity which displays a login screen to the user, offering registration as + * well. + */ +public class LoginActivity extends Activity { + public static final String ARG_ACCOUNT_TYPE = "accountType"; + public static final String ARG_AUTH_TYPE = "authType"; + public static final String ARG_ADDING_NEW_ACCOUNT = "newAccount"; + public static final String ARG_ACCOUNT_NAME = "accountName"; + public static final String KEY_ACCOUNT_AUTHENTICATOR_RESPONSE = "authResponse"; + public static final String KEY_ERROR_MESSAGE = "ERR_MSG"; + + public final static String PARAM_USER_PASS = "USER_PASS"; + + private final int REQ_SIGNUP = 1; + + private final String TAG = this.getClass().getSimpleName(); + + private AccountManager mAccountManager; + private String mAuthTokenType; + /** + * 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" + }; + + /** + * The default email to populate the email field with. + */ + public static final String EXTRA_EMAIL = "com.example.android.authenticatordemo.extra.EMAIL"; + + /** + * Keep track of the login task to ensure we can cancel it if requested. + */ + private UserLoginTask mAuthTask = null; + + // Values for email and password at the time of the login attempt. + private String mEmail; + private String mPassword; + + // UI references. + private EditText mEmailView; + private EditText mPasswordView; + private View mLoginFormView; + private View mLoginStatusView; + private TextView mLoginStatusMessageView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_login); + + // Set up the login form. + mEmail = getIntent().getStringExtra(EXTRA_EMAIL); + mEmailView = (EditText) findViewById(R.id.email); + mEmailView.setText(mEmail); + + mPasswordView = (EditText) findViewById(R.id.password); + mPasswordView.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) { + if (id == R.id.login || id == EditorInfo.IME_NULL) { + attemptLogin(); + return true; + } + return false; + } + }); + + mLoginFormView = findViewById(R.id.login_form); + mLoginStatusView = findViewById(R.id.login_status); + mLoginStatusMessageView = (TextView) findViewById(R.id.login_status_message); + + findViewById(R.id.sign_in_button).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + attemptLogin(); + } + }); + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.login, menu); + return true; + } + + /** + * 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. + */ + public void attemptLogin() { + if (mAuthTask != null) { + return; + } + + // Reset errors. + mEmailView.setError(null); + mPasswordView.setError(null); + + // Store values at the time of the login attempt. + mEmail = mEmailView.getText().toString(); + mPassword = mPasswordView.getText().toString(); + + boolean cancel = false; + View focusView = null; + + // Check for a valid password. + if (TextUtils.isEmpty(mPassword)) { + mPasswordView.setError(getString(R.string.error_field_required)); + focusView = mPasswordView; + cancel = true; + } else if (mPassword.length() < 4) { + mPasswordView.setError(getString(R.string.error_invalid_password)); + focusView = mPasswordView; + cancel = true; + } + + // Check for a valid email address. + if (TextUtils.isEmpty(mEmail)) { + mEmailView.setError(getString(R.string.error_field_required)); + focusView = mEmailView; + cancel = true; + } else if (!mEmail.contains("@")) { + 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. + mLoginStatusMessageView.setText(R.string.login_progress_signing_in); + showProgress(true); + mAuthTask = new UserLoginTask(); + mAuthTask.execute((Void) null); + } + } + + /** + * Shows the progress UI and hides the login form. + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2) + private 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); + + mLoginStatusView.setVisibility(View.VISIBLE); + mLoginStatusView.animate() + .setDuration(shortAnimTime) + .alpha(show ? 1 : 0) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mLoginStatusView.setVisibility(show ? View.VISIBLE : View.GONE); + } + }); + + mLoginFormView.setVisibility(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); + } + }); + } else { + // The ViewPropertyAnimator APIs are not available, so simply show + // and hide the relevant UI components. + mLoginStatusView.setVisibility(show ? View.VISIBLE : View.GONE); + mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); + } + } + + /** + * Represents an asynchronous login/registration task used to authenticate + * the user. + */ + public class UserLoginTask extends AsyncTask { + @Override + protected Boolean doInBackground(Void... params) { + // TODO: attempt authentication against a network service. + + try { + // Simulate network access. + Thread.sleep(2000); + } catch (InterruptedException e) { + return false; + } + + 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); + } + } + + // TODO: register the new account here. + return true; + } + + @Override + protected void onPostExecute(final Boolean success) { + mAuthTask = null; + showProgress(false); + + if (success) { + finish(); + } else { + mPasswordView.setError(getString(R.string.error_incorrect_password)); + mPasswordView.requestFocus(); + } + } + + @Override + protected void onCancelled() { + mAuthTask = null; + showProgress(false); + } + } +} diff --git a/katana/src/main/java/org/techniche/technothlon/katana/SplashScreen.java b/katana/src/main/java/org/techniche/technothlon/katana/SplashScreen.java index 5477c65..defe706 100644 --- a/katana/src/main/java/org/techniche/technothlon/katana/SplashScreen.java +++ b/katana/src/main/java/org/techniche/technothlon/katana/SplashScreen.java @@ -8,9 +8,6 @@ import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.ImageView; -import org.techniche.technothlon.katana.tcd.TCDContent; - -import java.util.Date; public class SplashScreen extends Activity { @@ -41,33 +38,23 @@ protected void onCreate(Bundle savedInstanceState) { @Override protected void onStart() { super.onStart(); - final long timeStart = (new Date()).getTime(); - (new TCDContent.TCDLoader() { + (new AsyncTask(){ @Override - public void finished(int result) { - long timeEnd = (new Date()).getTime(); - long timeTaken = Math.abs(timeEnd - timeStart); - if(timeTaken < 4000) { - (new AsyncTask(){ - @Override - protected String doInBackground(Object... params) { - try { - Thread.sleep((Long) params[0], 0); - } catch (InterruptedException e) { - e.printStackTrace(); - } - return null; - } - - @Override - protected void onPostExecute(String s) { - super.onPostExecute(s); - proceed(); - } - }).execute(4000 - timeTaken); + protected String doInBackground(Object... params) { + try { + Thread.sleep(4000); + } catch (InterruptedException e) { + e.printStackTrace(); } + return null; + } + + @Override + protected void onPostExecute(String s) { + super.onPostExecute(s); + proceed(); } - }).execute(getApplicationContext()); + }).execute(); } private void proceed() { diff --git a/katana/src/main/java/org/techniche/technothlon/katana/account/AccountConstants.java b/katana/src/main/java/org/techniche/technothlon/katana/account/AccountConstants.java new file mode 100644 index 0000000..2c6b052 --- /dev/null +++ b/katana/src/main/java/org/techniche/technothlon/katana/account/AccountConstants.java @@ -0,0 +1,29 @@ +package org.techniche.technothlon.katana.account; + +/** + * Created by kAd on 29/12/13. + * Part of org.techniche.technothlon.katana.account + */ +public class AccountConstants { + public static final String loginUrl = "http://www.technothlon.techniche.org/oauth2/login"; + /** + * Account type id + */ + public static final String ACCOUNT_TYPE = "technothlon.techniche.org"; + + /** + * Account name + */ + public static final String ACCOUNT_NAME = "Technothlon"; + + /** + * Auth token types + */ + public static final String AUTHTOKEN_TYPE_READ_ONLY = "Read only"; + public static final String AUTHTOKEN_TYPE_READ_ONLY_LABEL = "Read only access to an Udinic account"; + + public static final String AUTHTOKEN_TYPE_FULL_ACCESS = "Full access"; + public static final String AUTHTOKEN_TYPE_FULL_ACCESS_LABEL = "Full access to an Udinic account"; + + public static final ServerAuthenticate sServerAuthenticate = new ParseComServerAuthenticate(); +} diff --git a/katana/src/main/java/org/techniche/technothlon/katana/account/Authenticator.java b/katana/src/main/java/org/techniche/technothlon/katana/account/Authenticator.java new file mode 100644 index 0000000..412ee3f --- /dev/null +++ b/katana/src/main/java/org/techniche/technothlon/katana/account/Authenticator.java @@ -0,0 +1,123 @@ +package org.techniche.technothlon.katana.account; + +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 android.util.Log; + +import org.techniche.technothlon.katana.LoginActivity; + +/** + * Created by Rahul Kadyan on 28/12/13. + * Part of org.techniche.technothlon.katana.account + */ +public class Authenticator extends AbstractAccountAuthenticator { + private final Context context; + private String TAG = "Authenticator"; + + public Authenticator(Context context) { + super(context); + this.context = context; + } + + @Override + public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { + return null; + } + + @Override + public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException { + final Intent intent = new Intent(context, LoginActivity.class); + intent.putExtra(LoginActivity.ARG_ACCOUNT_TYPE, accountType); + intent.putExtra(LoginActivity.ARG_AUTH_TYPE, authTokenType); + intent.putExtra(LoginActivity.ARG_ADDING_NEW_ACCOUNT, true); + final Bundle bundle = new Bundle(); + bundle.putParcelable(AccountManager.KEY_INTENT, intent); + return bundle; + } + + @Override + public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException { + return null; + } + + @Override + public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException { + Log.d("katana", TAG + "> getAuthToken"); + + if (!authTokenType.equals(AccountConstants.AUTHTOKEN_TYPE_READ_ONLY) && !authTokenType.equals(AccountConstants.AUTHTOKEN_TYPE_FULL_ACCESS)) { + final Bundle result = new Bundle(); + result.putString(AccountManager.KEY_ERROR_MESSAGE, "invalid authTokenType"); + return result; + } + + final AccountManager accountManager = AccountManager.get(context); + + assert accountManager != null; + String authToken = accountManager.peekAuthToken(account, authTokenType); + + // Lets give another try to authenticate the user + if (TextUtils.isEmpty(authToken)) { + final String password = accountManager.getPassword(account); + if (password != null) { + if (password != null) { + try { + Log.d("katana", TAG + "> re-authenticating with the existing password"); + authToken = AccountConstants.sServerAuthenticate.userSignIn(context, account.name, password, authTokenType); + } catch (Exception 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(context, LoginActivity.class); + intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); + intent.putExtra(LoginActivity.ARG_ACCOUNT_TYPE, account.type); + intent.putExtra(LoginActivity.ARG_AUTH_TYPE, authTokenType); + intent.putExtra(LoginActivity.ARG_ACCOUNT_NAME, account.name); + final Bundle bundle = new Bundle(); + bundle.putParcelable(AccountManager.KEY_INTENT, intent); + return bundle; + } + + @Override + public String getAuthTokenLabel(String authTokenType) { + if (AccountConstants.AUTHTOKEN_TYPE_FULL_ACCESS.equals(authTokenType)) { + return AccountConstants.AUTHTOKEN_TYPE_FULL_ACCESS_LABEL; + } else if (AccountConstants.AUTHTOKEN_TYPE_READ_ONLY.equals(authTokenType)) { + return AccountConstants.AUTHTOKEN_TYPE_READ_ONLY_LABEL; + } else + return authTokenType + " (Technothlon)"; + } + + @Override + public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException { + return null; + } + + @Override + public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException { + final Bundle result = new Bundle(); + result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false); + return result; + } +} diff --git a/katana/src/main/java/org/techniche/technothlon/katana/account/AuthenticatorService.java b/katana/src/main/java/org/techniche/technothlon/katana/account/AuthenticatorService.java new file mode 100644 index 0000000..71d85b9 --- /dev/null +++ b/katana/src/main/java/org/techniche/technothlon/katana/account/AuthenticatorService.java @@ -0,0 +1,22 @@ +package org.techniche.technothlon.katana.account; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; + +public class AuthenticatorService extends Service { + private Authenticator authenticator; + public AuthenticatorService() { + } + + @Override + public void onCreate() { + super.onCreate(); + authenticator = new Authenticator(this); + } + + @Override + public IBinder onBind(Intent intent) { + return authenticator.getIBinder(); + } +} diff --git a/katana/src/main/java/org/techniche/technothlon/katana/account/ParseComServerAuthenticate.java b/katana/src/main/java/org/techniche/technothlon/katana/account/ParseComServerAuthenticate.java new file mode 100644 index 0000000..e5f5a5d --- /dev/null +++ b/katana/src/main/java/org/techniche/technothlon/katana/account/ParseComServerAuthenticate.java @@ -0,0 +1,84 @@ +package org.techniche.technothlon.katana.account; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.util.Log; + +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; + +/** + * Created by kAd on 29/12/13. + * Part of org.techniche.technothlon.katana.account + */ +public class ParseComServerAuthenticate implements ServerAuthenticate { + private static final String KATANA_SECRET_KEY = ""; // TODO add secret key on production + @Override + public String userSignIn(Context context, String user, String pass, String authType) throws Exception { + Log.d("katana", "userSignIn"); + String accessToken = ""; + ConnectivityManager connMgr = (ConnectivityManager) + context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); + if (networkInfo != null && networkInfo.isConnected()) { + String query = null; + try { + query = String.format("client_id=%s&client_secret=%s&username=%s&password=%s&scope=%s", + "katana", KATANA_SECRET_KEY,URLEncoder.encode(user, "UTF-8"), URLEncoder.encode(pass, "UTF-8"), + URLEncoder.encode("basic accesstechnopedia accessresults updateprofile", "URF-8")); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + URL url = new URL(AccountConstants.loginUrl); + + HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection(); + + httpURLConnection.setRequestMethod("POST"); + httpURLConnection.setReadTimeout(10000); + httpURLConnection.setConnectTimeout(15000); + httpURLConnection.setDoInput(true); + httpURLConnection.setDoOutput(true); + + OutputStreamWriter writer = new OutputStreamWriter(httpURLConnection.getOutputStream()); + writer.write(query); + writer.flush(); + + int responseCode = httpURLConnection.getResponseCode(); + if (responseCode != 200) { + throw new Exception("Error signing-in [" + responseCode + "] - " + httpURLConnection.getResponseMessage()); + } + InputStream inputStream = httpURLConnection.getInputStream(); + String result = readTextResponse(inputStream); + JSONObject jsonObject = new JSONObject(result); + accessToken = jsonObject.getString("access_token"); + } else { + Log.d("katana", "Network Not Available"); + } + return accessToken; + } + + private String readTextResponse(InputStream inputStream) throws IOException { + Reader in = new InputStreamReader(inputStream); + BufferedReader bufferedreader = new BufferedReader(in); + StringBuilder stringBuilder = new StringBuilder(); + + String stringReadLine; + + while ((stringReadLine = bufferedreader.readLine()) != null) { + stringBuilder.append(stringReadLine); + } + + return stringBuilder.toString(); + } +} diff --git a/katana/src/main/java/org/techniche/technothlon/katana/account/ServerAuthenticate.java b/katana/src/main/java/org/techniche/technothlon/katana/account/ServerAuthenticate.java new file mode 100644 index 0000000..1fca429 --- /dev/null +++ b/katana/src/main/java/org/techniche/technothlon/katana/account/ServerAuthenticate.java @@ -0,0 +1,11 @@ +package org.techniche.technothlon.katana.account; + +import android.content.Context; + +/** + * Created by kAd on 29/12/13. + * Part of org.techniche.technothlon.katana.account + */ +public interface ServerAuthenticate { + public String userSignIn( Context context, final String user, final String pass, String authType) throws Exception; +} diff --git a/katana/src/main/res/layout/activity_login.xml b/katana/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..6737336 --- /dev/null +++ b/katana/src/main/res/layout/activity_login.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + +