package com.nasageek.utexasutilities.activities; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.drawable.TransitionDrawable; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.v4.app.ActivityOptionsCompat; import android.support.v4.view.MenuItemCompat; import android.support.v7.app.AlertDialog; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.View.OnFocusChangeListener; import android.view.View.OnTouchListener; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.Toast; import com.crashlytics.android.Crashlytics; import com.nasageek.utexasutilities.AnalyticsHandler; import com.nasageek.utexasutilities.AsyncTask; import com.nasageek.utexasutilities.AuthCookie; import com.nasageek.utexasutilities.ChangeLogCompat; import com.nasageek.utexasutilities.ChangeableContextTask; import com.nasageek.utexasutilities.MyBus; import com.nasageek.utexasutilities.R; import com.nasageek.utexasutilities.UTilitiesApplication; import com.nasageek.utexasutilities.Utility; import com.squareup.otto.Subscribe; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import static com.nasageek.utexasutilities.UTilitiesApplication.UTD_AUTH_COOKIE_KEY; /** * Main entry point for UTilities. Allows the user to log in and contains a dashboard * of buttons to launch UT web services. */ public class UTilitiesActivity extends BaseActivity { private static final float CHECK_TRANSLUCENT_OPACITY = 0.3f; private final static int BUTTON_ANIMATION_DURATION = 90; private SharedPreferences settings; private Toast message; private ImageView scheduleCheck, balanceCheck, dataCheck; private AlertDialog nologin; private UTilitiesApplication app; private List<AsyncTask> loginTasks; private UpdateUiTask updateUiTask; private AuthCookie authCookie; private boolean loginFailed; private ImageView[] featureButtons; private View.OnClickListener enabledFeatureButtonListener; private View.OnClickListener disabledFeatureButtonListener; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); MyBus.getInstance().register(this); if (!isTaskRoot()) { finish(); return; } app = (UTilitiesApplication) getApplication(); authCookie = app.getAuthCookie(UTD_AUTH_COOKIE_KEY); loginTasks = (List<AsyncTask>) getLastCustomNonConfigurationInstance(); if (loginTasks != null) { updateUiTask = (UpdateUiTask) loginTasks.get(0); if (updateUiTask != null) { updateUiTask.setContext(this); } } if (savedInstanceState != null) { loginFailed = savedInstanceState.getBoolean("loginFailed"); } else { loginFailed = false; } settings = PreferenceManager.getDefaultSharedPreferences(this.getBaseContext()); setContentView(R.layout.main); enabledFeatureButtonListener = v -> { DashboardButtonData data = (DashboardButtonData) v.getTag(); Intent intent = null; // null cookie means the service doesn't need an EID if (data.authCookie == null) { intent = data.intent; } else { if (settings.getBoolean(getString(R.string.pref_logintype_key), false)) { // persistent login if ((!data.authCookie.hasCookieBeenSet() || isLoggingIn()) && isLoginRequired()) { showLoginFirstToast(); } else { intent = data.intent; } } else { // temp login if (!data.authCookie.hasCookieBeenSet()) { Intent login = new Intent(UTilitiesActivity.this, LoginActivity.class); login.putExtra("activity", data.intent.getComponent() .getClassName()); intent = login; } else { intent = data.intent; } } } if (intent != null) { Bundle opts = ActivityOptionsCompat.makeScaleUpAnimation(v, 0, 0, v.getWidth(), v.getHeight()).toBundle(); startActivity(intent, opts); } }; disabledFeatureButtonListener = v -> { AlertDialog.Builder featureDisabledBuilder = new AlertDialog.Builder(UTilitiesActivity.this); featureDisabledBuilder .setMessage("This feature has been disabled due to a failed login, would " + "you like to try logging in again?") .setPositiveButton("Retry", (dialog, id) -> { ((DashboardButtonData) v.getTag()).loginProgress .setVisibility(View.VISIBLE); loginTasks = new ArrayList<>(); CountDownLatch loginLatch = new CountDownLatch(1); updateUiTask = new UpdateUiTask(UTilitiesActivity.this); Utility.parallelExecute(updateUiTask, loginLatch); loginTasks.add(updateUiTask); AuthCookie cookie = ((DashboardButtonData) v.getTag()).authCookie; LoginTask loginTask = new LoginTask(loginLatch); Utility.parallelExecute(loginTask, cookie); loginTasks.add(loginTask); }) .setNegativeButton("Cancel", null); AlertDialog featureDisabled = featureDisabledBuilder.create(); featureDisabled.show(); }; setupDashBoardButtons(); setLoginProgressBarVisiblity(isLoggingIn()); // use one Activity-wide Toast so they don't stack up message = Toast.makeText(this, R.string.login_first, Toast.LENGTH_SHORT); handleUnencryptedPassword(); handleFirstLaunch(); if (settings.getBoolean("autologin", false) && !isLoggingIn() && !app.anyCookiesSet()) { login(); } } // simple struct-like class to help handle related data class DashboardButtonData { public Intent intent; public int imageButtonId; public AuthCookie authCookie; public ImageView checkOverlay; public ProgressBar loginProgress; public boolean enabled; // for authenticated services public DashboardButtonData(Intent intent, int id, AuthCookie authCookie, ImageView check, ProgressBar progress, boolean enabled) { this.intent = intent; this.imageButtonId = id; this.authCookie = authCookie; this.checkOverlay = check; this.loginProgress = progress; this.enabled = enabled; } // for unauthenticated services public DashboardButtonData(Intent intent, int id) { this(intent, id, null, null, null, true); } } /** * Set up the dashboard of buttons to launch the various services of the * app. This covers things like touch/focus listeners, authentication, * intent to launch, etc. */ private void setupDashBoardButtons() { scheduleCheck = (ImageView) findViewById(R.id.scheduleCheck); balanceCheck = (ImageView) findViewById(R.id.balanceCheck); dataCheck = (ImageView) findViewById(R.id.dataCheck); ProgressBar scheduleProgress = (ProgressBar) findViewById(R.id.scheduleProgress); ProgressBar balanceProgress = (ProgressBar) findViewById(R.id.balanceProgress); ProgressBar dataProgress = (ProgressBar) findViewById(R.id.dataProgress); final Intent schedule = new Intent(this, ScheduleActivity.class); final Intent balance = new Intent(this, BalanceActivity.class); final Intent map = new Intent(this, CampusMapActivity.class); final Intent data = new Intent(this, DataUsageActivity.class); final Intent menu = new Intent(this, MenuActivity.class); DashboardButtonData buttonData[] = new DashboardButtonData[6]; buttonData[0] = new DashboardButtonData(schedule, R.id.schedule_button, authCookie, scheduleCheck, scheduleProgress, !loginFailed); buttonData[1] = new DashboardButtonData(balance, R.id.balance_button, authCookie, balanceCheck, balanceProgress, !loginFailed); buttonData[2] = new DashboardButtonData(data, R.id.data_button, authCookie, dataCheck, dataProgress, !loginFailed); buttonData[3] = new DashboardButtonData(map, R.id.map_button); buttonData[4] = new DashboardButtonData(menu, R.id.menu_button); featureButtons = new ImageView[5]; for (int i = 0; i < 5; i++) { ImageView ib = (ImageView) findViewById(buttonData[i].imageButtonId); ib.setOnTouchListener(new ImageButtonTouchListener( (TransitionDrawable) ib.getDrawable())); ib.setOnFocusChangeListener(new ImageButtonFocusListener()); ib.setTag(buttonData[i]); if (buttonData[i].enabled) { enableFeature(ib); } else { disableFeature(ib); } featureButtons[i] = ib; } } /** * FocusListener that triggers a TransitionDrawable when the item goes * in and out of focus. */ private class ImageButtonFocusListener implements OnFocusChangeListener { @Override public void onFocusChange(View v, boolean hasFocus) { if (hasFocus) { ((TransitionDrawable) ((ImageView) v).getDrawable()) .startTransition(BUTTON_ANIMATION_DURATION); } else { ((TransitionDrawable) ((ImageView) v).getDrawable()) .reverseTransition(BUTTON_ANIMATION_DURATION); } } } /** * TouchListener that triggers a TransitionDrawable on touch and release. */ private class ImageButtonTouchListener implements OnTouchListener { private boolean buttonPressed = false; private TransitionDrawable crossfade; public ImageButtonTouchListener(TransitionDrawable transDrawable) { crossfade = transDrawable; } @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: if (!buttonPressed) { buttonPressed = true; crossfade.startTransition(BUTTON_ANIMATION_DURATION); } break; case MotionEvent.ACTION_UP: if (buttonPressed) { buttonPressed = false; crossfade.reverseTransition(BUTTON_ANIMATION_DURATION); } break; case MotionEvent.ACTION_MOVE: final int[] states = v.getDrawableState(); boolean pressedStateFound = false; for (int state : states) { if (state == android.R.attr.state_pressed) { pressedStateFound = true; if (!buttonPressed) { buttonPressed = true; crossfade.startTransition(BUTTON_ANIMATION_DURATION); } break; } } if (!pressedStateFound && buttonPressed) { buttonPressed = false; crossfade.reverseTransition(BUTTON_ANIMATION_DURATION); } break; } return false; } } @Override public boolean onCreateOptionsMenu(Menu menu) { if (isFinishing()) { // otherwise we crash if we try to finish() in onCreate return false; } MenuInflater inflater = this.getMenuInflater(); inflater.inflate(R.menu.main_menu, menu); // update the displayed login state if (settings.getBoolean(getString(R.string.pref_logintype_key), false)) { if (!isLoggingIn()) { if (authCookie.hasCookieBeenSet()) { replaceLoginButton(menu, R.id.logout_button, "Log out"); } else { replaceLoginButton(menu, R.id.login_button, "Log in"); } } else { replaceLoginButton(menu, R.id.cancel_button, "Cancel"); } } else { if (authCookie.hasCookieBeenSet()) { replaceLoginButton(menu, R.id.logout_button, "Log out"); } else { menu.removeGroup(R.id.login_menu_group); } } return true; } /** * Replace current login button with another (typically "Log-in", "Cancel", or "Log out") * @param menu menu to add to * @param id id of button being added * @param text text of the button being added */ private void replaceLoginButton(Menu menu, int id, String text) { menu.removeGroup(R.id.login_menu_group); menu.add(R.id.login_menu_group, id, Menu.NONE, text); MenuItem item = menu.findItem(id); MenuItemCompat.setShowAsAction(item, MenuItemCompat.SHOW_AS_ACTION_ALWAYS); } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); switch (id) { case R.id.settings: loadSettings(); return true; case R.id.login_button: login(); supportInvalidateOptionsMenu(); return true; case R.id.logout_button: AnalyticsHandler.trackLogoutEvent(); logout(); supportInvalidateOptionsMenu(); return true; case R.id.cancel_button: cancelLogin(); supportInvalidateOptionsMenu(); return true; default: return super.onOptionsItemSelected(item); } } @Override public Object onRetainCustomNonConfigurationInstance() { return loginTasks; } /** * Show "first launch" dialogs. Either the changelog for the first launch after an update * or the actual first-launch dialog letting the user known they should save their * credentials to get full use of the app. */ private void handleFirstLaunch() { if (!settings.contains("firstRun")) { // first launch ever AlertDialog.Builder nologin_builder = new AlertDialog.Builder(this); nologin_builder .setMessage( "This is your first time running UTilities; why don't you try" + " logging in to get the most use out of the app?") .setCancelable(false) .setPositiveButton("Ok", (dialog, id) -> { loadSettings(); }) .setNegativeButton("No thanks", null); nologin = nologin_builder.create(); nologin.show(); settings.edit().putBoolean("firstRun", false).apply(); Utility.id(this); } else { ChangeLogCompat cl = new ChangeLogCompat(this); if (cl.isFirstRun()) { // first launch after an update // this will actually show after the second launch if it's a fresh install cl.getFullLogDialogCompat().show(); } } } /** * Legacy method, probably not used anymore. * Removes any unencrypted password and stores a boolean so this doesn't happen again * and shows the user a dialog informing them of what's happened. */ private void handleUnencryptedPassword() { if (!settings.contains("encryptedpassword") && settings.contains("firstRun") && settings.contains("password")) { settings.edit().remove("password").apply(); settings.edit().putBoolean("encryptedpassword", true).apply(); // turn off autologin, they can re-activate it when they enter their password settings.edit().putBoolean("autologin", false).apply(); showUnencryptedPasswordDialog(); } } /** * Show dialog informing the user that their password has been reset. */ private void showUnencryptedPasswordDialog() { AlertDialog.Builder passwordcleared_builder = new AlertDialog.Builder(this); passwordcleared_builder .setMessage( "With this update to UTilities, your stored password will be " + "encrypted. Your currently stored password has been wiped " + "for security purposes and you will need to re-enter it.") .setCancelable(true) .setPositiveButton("Ok", null); AlertDialog passwordcleared = passwordcleared_builder.create(); passwordcleared.show(); } private void loadSettings() { final Intent pref_intent = new Intent(this, PreferenceActivity.class); startActivity(pref_intent); } private boolean isLoggingIn() { // updateUiTask is null before login has begun return updateUiTask != null && updateUiTask.getStatus() == AsyncTask.Status.RUNNING && !updateUiTask.isCancelled(); } /** * Call login on the given AuthCookie and decrement the given CountDownLatch afterwards. * This doesn't really need to be an AsyncTask, that's just what I'm most familiar with. */ static class LoginTask extends AsyncTask<AuthCookie, Void, Boolean> { private CountDownLatch loginLatch; private AuthCookie cookie; public LoginTask(CountDownLatch loginLatch) { this.loginLatch = loginLatch; } @Override protected Boolean doInBackground(AuthCookie... params) { cookie = params[0]; Boolean result = false; try { /* We can ignore the return value of login() because UpdateUITask ensures all of the cookies have been set before completing the login. */ result = cookie.login(); } catch (IOException e) { /* TODO: Inform the user that the login request failed due to a network error. This will require a change to AuthCookie or some other sort of shared state between LoginTask and UpdateUITask. */ e.printStackTrace(); } loginLatch.countDown(); return result; } @Override protected void onPostExecute(Boolean result) { MyBus.getInstance().post(new LoginFinishedEvent(cookie.getPrefKey(), result)); } } /** * AsyncTask that waits on the given CountDownLatch to deplete. It then checks to see if all * of the Activity's AuthCookies have been set and changes the UI accordingly. */ static class UpdateUiTask extends AsyncTask<CountDownLatch, Void, Void> implements ChangeableContextTask { private UTilitiesActivity mActivity; public UpdateUiTask(UTilitiesActivity act) { mActivity = act; } @Override protected Void doInBackground(CountDownLatch... params) { CountDownLatch loginLatch = params[0]; try { loginLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } return null; } @Override protected void onPostExecute(Void result) { if (!mActivity.authCookie.hasCookieBeenSet()) { Toast.makeText(mActivity, "One or more services could not log in and have been disabled", Toast.LENGTH_SHORT).show(); } /* trick to make sure that the login is seen as "done" before onCreateOptionsMenu() is called. */ cancel(false); mActivity.supportInvalidateOptionsMenu(); mActivity.setLoginProgressBarVisiblity(false); } @Override public void setContext(Context con) { mActivity = (UTilitiesActivity) con; } } /** * Perform a login with the user's saved credentials. */ private void login() { SharedPreferences sp = app.getSecurePreferences(); if (sp.getString("password", "") == null) { // :( encryption broken in some way // Disable persistent login and let the user know something's up settings.edit().putBoolean(getString(R.string.pref_logintype_key), false).apply(); resetChecks(); Toast.makeText(this, "Uh oh, password encryption is broken on your device! " + "Persistent login has been temporarily disabled.", Toast.LENGTH_LONG).show(); Crashlytics.logException(new Exception("Password decryption failure")); } if (settings.getBoolean(getString(R.string.pref_logintype_key), false)) { if (!settings.contains("eid") || !sp.contains("password") || settings.getString("eid", "").equals("") || sp.getString("password", "").equals("")) { message.setText("Please enter your credentials to log in"); message.setDuration(Toast.LENGTH_LONG); message.show(); } else { setLoginProgressBarVisiblity(true); CountDownLatch loginLatch = new CountDownLatch(1); updateUiTask = new UpdateUiTask(this); Utility.parallelExecute(updateUiTask, loginLatch); loginTasks = new ArrayList<>(); loginTasks.add(updateUiTask); LoginTask loginTask = new LoginTask(loginLatch); Utility.parallelExecute(loginTask, authCookie); loginTasks.add(loginTask); } } } /** * Cancel the login process. */ private void cancelLogin() { for (AsyncTask task : loginTasks) { task.cancel(true); } logout(); setLoginProgressBarVisiblity(false); } /** * Log the user out of all UT web services. */ private void logout() { app.logoutAll(); for (ImageView ib : featureButtons) { enableFeature(ib); if (((DashboardButtonData) ib.getTag()).loginProgress != null) { ((DashboardButtonData) ib.getTag()).loginProgress.setVisibility(View.GONE); } } loginFailed = false; resetChecks(); } private void disableFeature(final ImageView featureButton) { // Some sort of bug in Android 4.x causes the ImageView to disappear if a ColorMatrix // is applied, so only do it in 2.3/5.0+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { ColorMatrix matrix = new ColorMatrix(); matrix.setSaturation(0); ColorMatrixColorFilter filter = new ColorMatrixColorFilter(matrix); featureButton.setColorFilter(filter); } Utility.setImageAlpha(featureButton, 75); featureButton.setOnClickListener(disabledFeatureButtonListener); } private void enableFeature(final ImageView featureButton) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { featureButton.clearColorFilter(); } Utility.setImageAlpha(featureButton, 255); featureButton.setOnClickListener(enabledFeatureButtonListener); } @Subscribe public void loginFinished(final LoginFinishedEvent lfe) { if (isFinishing()) { return; } boolean successful = lfe.loginSuccessful() || !isLoginRequired(); loginFailed = !successful; for (ImageView iv : featureButtons) { DashboardButtonData buttonData = (DashboardButtonData) iv.getTag(); if (buttonData.authCookie != null) { if (successful) { enableFeature(iv); } else { disableFeature(iv); } buttonData.enabled = successful; buttonData.loginProgress.setVisibility(View.GONE); } } } private void showLoginFirstToast() { message.setText(R.string.login_first); message.setDuration(Toast.LENGTH_SHORT); message.show(); } private boolean isLoginRequired() { return !settings.getBoolean("dont_require_login", false); } @Override public void onResume() { super.onResume(); supportInvalidateOptionsMenu(); if (!settings.getBoolean(getString(R.string.pref_logintype_key), false)) { for (ImageView iv : featureButtons) { enableFeature(iv); } } resetChecks(); } @Override public void onPause() { super.onPause(); if (nologin != null) { if (nologin.isShowing()) { nologin.dismiss(); } } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean("loginFailed", loginFailed); } @Override protected void onDestroy() { MyBus.getInstance().unregister(this); super.onDestroy(); } /** * Reset the checkmark overlays that act as temp login indicators to * ensure they show the correct login status. */ private void resetChecks() { boolean persistentLoginEnabled = settings.getBoolean(getString(R.string.pref_logintype_key), false); for (ImageView featureButton : featureButtons) { DashboardButtonData button = (DashboardButtonData) featureButton.getTag(); if (button.authCookie != null) { button.checkOverlay.setVisibility(persistentLoginEnabled ? View.GONE : View.VISIBLE); if (button.authCookie.hasCookieBeenSet()) { button.checkOverlay.setAlpha(1f); } else { button.checkOverlay.setAlpha(CHECK_TRANSLUCENT_OPACITY); } } } } private void setLoginProgressBarVisiblity(Boolean showOrHide) { for (ImageView featureButton : featureButtons) { DashboardButtonData dbd = (DashboardButtonData) featureButton.getTag(); if (dbd.loginProgress != null) { dbd.loginProgress. setVisibility(showOrHide ? View.VISIBLE : View.GONE); } } } static class LoginFinishedEvent { private String service; private boolean successful; public LoginFinishedEvent(String service, boolean successful) { this.service = service; this.successful = successful; } public String getService() { return service; } public boolean loginSuccessful() { return successful; } } }