package com.dozuki.ifixit.ui; import android.content.Intent; import android.graphics.drawable.ColorDrawable; import android.os.Build; import android.os.Bundle; import android.support.v4.app.Fragment; import android.util.TypedValue; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import com.actionbarsherlock.app.ActionBar; import com.actionbarsherlock.app.SherlockFragmentActivity; import com.dozuki.ifixit.App; import com.dozuki.ifixit.R; import com.dozuki.ifixit.model.dozuki.Site; import com.dozuki.ifixit.model.dozuki.SiteChangedEvent; import com.dozuki.ifixit.model.user.LoginEvent; import com.dozuki.ifixit.model.user.User; import com.dozuki.ifixit.ui.auth.LoginFragment; import com.dozuki.ifixit.util.ImageSizes; import com.dozuki.ifixit.util.PicassoUtils; import com.dozuki.ifixit.util.ViewServer; import com.dozuki.ifixit.util.api.Api; import com.dozuki.ifixit.util.api.ApiEvent; import com.squareup.otto.DeadEvent; import com.squareup.otto.Subscribe; /** * Base Activity that performs various functions that all Activities in this app * should do. Such as: * * Registering for the event bus. Setting the current site's theme. Finishing * the Activity if the user logs out but the Activity requires authentication. */ public abstract class BaseActivity extends SherlockFragmentActivity { protected static final String LOADING = "LOADING_FRAGMENT"; private static final String ACTIVITY_ID = "ACTIVITY_ID"; private static final String USERID = "USERID"; private static final String SITE = "SITE"; // If an Intent has a site argument it will change sites before displaying any content. private static final String SITE_ARGUMENT = "SITE_ARGUMENT"; public static final int GOOGLE_SIGN_IN_REQUEST_CODE = 8347; private static final int LOGGED_OUT_USERID = -1; private int mActivityid; private int mUserid; private Site mSite; private LoginFragment.GoogleSignInActivityResult mPendingGoogleSigninResult; /** * This is incredibly hacky. The issue is that Otto does not search for @Subscribed * methods in parent classes because the performance hit is far too big for * Android because of the deep inheritance with the framework and views. * Because of this, @Subscribed methods on BaseActivity itself don't get * registered. The workaround is to make an anonymous object that is registered * on behalf of the parent class. Workaround courtesy of: * https://github.com/square/otto/issues/26 * * Note: The '@SuppressWarnings("unused")' is to prevent * warnings that are incorrect (the methods *are* actually used. */ private Object mBaseActivityListener = new Object() { @SuppressWarnings("unused") @Subscribe public void onLoginEvent(LoginEvent.Login event) { onLogin(event); } @SuppressWarnings("unused") @Subscribe public void onLogoutEvent(LoginEvent.Logout event) { onLogout(event); } @SuppressWarnings("unused") @Subscribe public void onCancelEvent(LoginEvent.Cancel event) { onCancelLogin(event); } @SuppressWarnings("unused") @Subscribe public void onUnauthorized(ApiEvent.Unauthorized event) { openLoginDialogIfLoggedOut(); } @SuppressWarnings("unused") @Subscribe public void onApiCall(ApiEvent.ActivityProxy activityProxy) { if (activityProxy.getActivityid() == mActivityid) { // Send the real event off to the real handler. App.getBus().post(activityProxy.getApiEvent()); } else { // Send the event back to Api so it can retry it for the // intended Activity. App.getBus().post(new DeadEvent(App.getBus(), activityProxy.getApiEvent())); } } @SuppressWarnings("unused") @Subscribe public void onSiteChanged(SiteChangedEvent event) { mSite = event.mSite; // Reset the userid so we don't erroneously finish the Activity. setUserid(); } }; @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); // LoginFragment doesn't get all of the onActivityResults for google sign in // so the activity needs to proxy them through but only after the LoginFragment has // been registered with the event bus. if (requestCode == GOOGLE_SIGN_IN_REQUEST_CODE) { mPendingGoogleSigninResult = new LoginFragment.GoogleSignInActivityResult(requestCode, resultCode, data); } } @Override public void onCreate(Bundle savedState) { App app = App.get(); Site currentSite = app.getSite(); if (savedState != null) { mActivityid = savedState.getInt(ACTIVITY_ID); mUserid = savedState.getInt(USERID); mSite = (Site)savedState.getSerializable(SITE); // If the site associated with this Activity is different than the current site, // set it to the one this Activity wants. Don't always do this because of the // overhead of reading the user from SharedPreferences. if (mSite.mSiteid != currentSite.mSiteid) { app.setSite(mSite); } } else { mActivityid = generateActivityid(); setUserid(); Site siteArgument = (Site)getIntent().getSerializableExtra(SITE_ARGUMENT); if (siteArgument != null && siteArgument.mSiteid != currentSite.mSiteid) { mSite = siteArgument; app.setSite(mSite); } else { mSite = app.getSite(); } } Site site = app.getSite(); ActionBar ab = getSupportActionBar(); ab.setDisplayHomeAsUpEnabled(true); /** * Set the current site's theme. Must be before onCreate because of * inflating views. */ setTheme(app.getSiteTheme()); // This doesn't work on on versions below ICS. Don't really care if the home button pressed state is the wrong // color on those devices so just ignore it. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { ((View)findViewById(android.R.id.home).getParent().getParent()).setBackgroundResource(R.drawable .item_background_holo_light); } if (site.actionBarUsesIcon()) { ab.setLogo(getResources().getIdentifier("icon", "drawable", getPackageName())); ab.setDisplayUseLogoEnabled(true); // Get the default action bar title resourceid int titleId = getResources().getIdentifier("action_bar_title", "id", "android"); // If it doesn't exist, use actionbarsherlocks if (titleId == 0) { titleId = com.actionbarsherlock.R.id.abs__action_bar_title; } TextView title = (TextView) findViewById(titleId); // If we were able to get the title element, set it to multi-line and a bit smaller text size so that long // site titles (i.e. Hypertherm Waterjet Mobile Assistant) and long guide titles fit nicely. if (title != null) { title.setSingleLine(false); title.setMaxLines(2); title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); } } else { ab.setDisplayUseLogoEnabled(false); ab.setDisplayShowTitleEnabled(false); ab.setDisplayShowCustomEnabled(true); ab.setIcon(new ColorDrawable(getResources().getColor(android.R.color.transparent))); View v = getLayoutInflater().inflate(R.layout.menu_title, null); ImageView customLogo = (ImageView) v.findViewById(R.id.custom_logo); TextView siteTitle = (TextView) v.findViewById(R.id.custom_site_title); if (site.mLogo != null) { PicassoUtils.with(this) .load(site.mLogo.getPath(ImageSizes.logo)) .error(R.drawable.logo_dozuki) .into(customLogo); customLogo.setVisibility(View.VISIBLE); siteTitle.setVisibility(View.GONE); } else { siteTitle.setText(site.mTitle); siteTitle.setVisibility(View.VISIBLE); customLogo.setVisibility(View.GONE); } v.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { onCustomMenuTitleClick(v); } }); ab.setCustomView(v); } super.onCreate(savedState); /** * There is another register call in onResume but we also need it here for the onUnauthorized * call that is usually triggered in onCreate of derived Activities. */ App.getBus().register(this); App.getBus().register(mBaseActivityListener); if (App.inDebug()) { ViewServer.get(this).addWindow(this); } Api.retryDeadEvents(this); } /** * Returns a unique integer for use as an activity id. */ private static int sActivityIdCounter = 0; private int generateActivityid() { return sActivityIdCounter++; } public int getActivityid() { return mActivityid; } public void setTitle(String title) { if (App.get().getSite().actionBarUsesIcon()) { getSupportActionBar().setTitle(title); } else { TextView titleView = ((TextView)getSupportActionBar().getCustomView(). findViewById(R.id.custom_page_title)); titleView.setText(title); } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putInt(ACTIVITY_ID, mActivityid); outState.putInt(USERID, mUserid); outState.putSerializable(SITE, mSite); } /** * If the user is coming back to this Activity make sure they still have * permission to view it. onRestoreInstanceState is for Activities that are * being recreated and onRestart is for Activities who are merely being * restarted. Unfortunately both are needed. */ @Override public void onRestoreInstanceState(Bundle savedState) { super.onRestoreInstanceState(savedState); finishActivityIfPermissionDenied(); } @Override public void onStart() { super.onStart(); overridePendingTransition(0, 0); } @Override public void onRestart() { super.onRestart(); finishActivityIfPermissionDenied(); } @Override public void onResume() { super.onResume(); App.getBus().register(this); App.getBus().register(mBaseActivityListener); if (App.inDebug()) { ViewServer.get(this).setFocusedWindow(this); } } @Override protected void onPostResume() { super.onPostResume(); /** * This covers missed events caused by dialogs or other views causing the * Activity's onPause method to be called which unregisters the Activity * as well as returning to an already running Activity via the back button. */ Api.retryDeadEvents(this); if (mPendingGoogleSigninResult != null) { App.getBus().post(mPendingGoogleSigninResult); mPendingGoogleSigninResult = null; } } @Override public void onDestroy() { super.onDestroy(); if (App.inDebug()) { ViewServer.get(this).removeWindow(this); } } @Override public void onPause() { super.onPause(); App.getBus().unregister(this); App.getBus().unregister(mBaseActivityListener); } public boolean openLoginDialogIfLoggedOut() { if (!App.get().isUserLoggedIn()) { LoginFragment.newInstance().show(getSupportFragmentManager(), "LoginFragment"); return true; } else { return false; } } public void onLogin(LoginEvent.Login event) { setUserid(); } public void onLogout(LoginEvent.Logout event) { /** * Check permissions before setting mUserid. Otherwise the Activity * will never be finished because mUserid matches the currently logged * in user. */ finishActivityIfPermissionDenied(); setUserid(); } public void onCancelLogin(LoginEvent.Cancel event) { finishActivityIfPermissionDenied(); } /** * Sets the userid to the currently logged in user's userid. */ private void setUserid() { User user = App.get().getUser(); mUserid = user == null ? LOGGED_OUT_USERID : user.getUserid(); } /** * Called when the custom menu title is clicked. This is only applicable * for Dozuki because of how the custom logo is displayed in the action bar. */ protected void onCustomMenuTitleClick(View v) { // Finish the Activity because this is the "up" action by default. finish(); } /** * Finishes the Activity if the user should be logged in but isn't. */ private void finishActivityIfPermissionDenied() { App app = App.get(); User user = app.getUser(); int currentUserid = user == null ? LOGGED_OUT_USERID : user.getUserid(); // Never finish the activity if the user is logging in. if (neverFinishActivityOnLogout() || app.isLoggingIn()) { return; } // Finish if the site is private or activity requires authentication. if ((currentUserid == LOGGED_OUT_USERID || currentUserid != mUserid) && (finishActivityIfLoggedOut() || !app.getSite().mPublic)) { finish(); } } /** * Returns true if the Activity should be finished if the user logs out or * cancels authentication. */ public boolean finishActivityIfLoggedOut() { return false; } /** * Returns true if the Activity should never be finished despite meeting * other conditions. * * This exists because of a race condition of sorts involving logging out of * private Dozuki sites. SiteListActivity can't reset the current site to * one that is public so it is erroneously finished unless flagged * otherwise. */ public boolean neverFinishActivityOnLogout() { return false; } public void showLoading(int container) { showLoading(container, getString(R.string.loading)); } public void showLoading(int container, String message) { getSupportFragmentManager().beginTransaction() .add(container, new LoadingFragment(message), LOADING) .commit(); } public void hideLoading() { Fragment loadingFragment = getSupportFragmentManager().findFragmentByTag(LOADING); if (loadingFragment != null) { // Because this is only hiding the loading fragment, it's fine to // commit with possible state loss. getSupportFragmentManager().beginTransaction() .remove(loadingFragment) .commitAllowingStateLoss(); } } public static Intent addSite(Intent intent, Site site) { intent.putExtra(SITE_ARGUMENT, site); return intent; } }