package com.airlocksoftware.hackernews.activity;
import com.crashlytics.android.Crashlytics;
import org.apache.commons.lang3.StringUtils;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.PointF;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.view.ViewPager;
import android.support.v4.view.ViewPager.OnPageChangeListener;
import android.view.View;
import android.view.ViewGroup;
import com.airlocksoftware.hackernews.R;
import com.airlocksoftware.hackernews.adapter.StoryAdapter;
import com.airlocksoftware.hackernews.data.UserPrefs;
import com.airlocksoftware.hackernews.fragment.CommentsFragment;
import com.airlocksoftware.hackernews.fragment.StoryFragment;
import com.airlocksoftware.hackernews.fragment.WebFragment;
import com.airlocksoftware.hackernews.interfaces.SharePopupInterface;
import com.airlocksoftware.hackernews.interfaces.TabletLayout;
import com.airlocksoftware.hackernews.loader.AsyncLoginUpdater;
import com.airlocksoftware.hackernews.model.Page;
import com.airlocksoftware.hackernews.model.Story;
import com.airlocksoftware.hackernews.view.SearchOverflow;
import com.airlocksoftware.hackernews.view.SharePopup;
import com.airlocksoftware.hackernews.view.TextSettingsOverflow;
import com.airlocksoftware.holo.actionbar.ActionBarView;
import com.airlocksoftware.holo.actionbar.TwoPaneController;
import com.airlocksoftware.holo.actionbar.TwoPaneController.Side;
import com.airlocksoftware.holo.actionbar.interfaces.ActionBarController;
import com.airlocksoftware.holo.adapters.FragmentPagerArrayAdapter;
import com.airlocksoftware.holo.checkable.CheckableView;
import com.airlocksoftware.holo.checkable.CheckableViewManager;
import com.airlocksoftware.holo.checkable.CheckableViewManager.OnCheckedViewChangedListener;
import com.airlocksoftware.holo.image.IconView;
import com.airlocksoftware.holo.interfaces.OnBackPressedListener;
import com.airlocksoftware.holo.utils.FragmentUtils;
import com.airlocksoftware.holo.utils.Utils;
import com.airlocksoftware.holo.utils.ViewUtils;
import com.airlocksoftware.holo.webview.DisableableViewPager;
import com.slidingmenu.lib.SlidingMenu;
/**
* The MainActivity is the host for the StoryFragment, CommentsFragment, and WebFragment.
*
* It's also responsible for:
* - deciding whether to show a two-pane or one-pane layout
* - managing the ActionBar tabs for Comments & Web Fragment
* - managing the SharePopup
* - managing the Search and TextSettings in the overflow menu
* - implementing the TabletLayout interface
* - managing communications between fragments by implementing any Callback interfaces it's child fragments require
* **/
public class MainActivity extends SlideoutMenuActivity implements SharePopupInterface, TabletLayout,
CommentsFragment.Callbacks, StoryFragment.Callbacks {
// Fragments
private CommentsFragment mCommentsFragment;
private WebFragment mWebFragment;
private StoryFragment mStoryFragment;
// ViewPager for CommentsFragment & WebFragment.
private DisableableViewPager mCommentsViewPager;
private FragmentPagerArrayAdapter mCommentsViewPagerAdapter;
// tabs & tab manager
private CheckableViewManager mTabManager;
private ViewGroup mTabs;
private SearchOverflow mSearch;
private TextSettingsOverflow mTextSettings;
private SharePopup mSharePopup;
private ActivityType mActivityType;
private Page mPage = Page.FRONT; // Which page to display in StoryFragment
private Story mStory; // Which story to display in CommentsFragment & WebFragment
private boolean mIsJobsPost = false; // if the current story is a YCombinator jobs post
private CommentsTab mInitialTabPosition = CommentsTab.COMMENTS;
private int mCurrentTabPosition = NO_CURRENT_POSITION;
private boolean mOpenInBrowser = false;
// Enums
/** The "type" of this Activity, based on screen width and the data we have. **/
public enum ActivityType {
TABLET, STORY, COMMENTS;
}
/** Which tab we are showing. **/
public enum CommentsTab {
COMMENTS, ARTICLE;
}
// Constants
private static final int VIEWPAGER_ID = R.id.viewpager;
private static final int NO_CURRENT_POSITION = -1;
private static final int COMMENTS_POS = 0;
private static final int ARTICLE_POS = 1;
private static final String STORY_FRAG_TAG = StoryFragment.class.getName();
private static final String COMMENTS_FRAG_TAG = FragmentUtils.makeFragmentPagerTag(VIEWPAGER_ID, COMMENTS_POS);
private static final String WEB_FRAG_TAG = FragmentUtils.makeFragmentPagerTag(VIEWPAGER_ID, ARTICLE_POS);
public static final String IS_COMMENTS_ACTIVITY = MainActivity.class.getSimpleName() + ".isCommentsActivity";
public static final String STORY = MainActivity.class.getSimpleName() + ".story";
public static final String INITIAL_TAB_POSITION = MainActivity.class.getSimpleName() + ".initialTabPosition";
public static final String PAGE = MainActivity.class.getSimpleName() + ".page";
private static final String TAG = MainActivity.class.getSimpleName();
// Listeners
private OnCheckedViewChangedListener mTabListener = new OnCheckedViewChangedListener() {
@Override
public void onCheckedViewChanged(CheckableViewManager manager, int newIndex, int oldIndex) {
// forwards tab check onto the ViewPager
mCommentsViewPager.setCurrentItem(newIndex);
}
};
private OnPageChangeListener mPageListener = new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrollStateChanged(int arg0) {
// No implementation necessary
}
@Override
public void onPageScrolled(int arg0, float arg1, int arg2) {
// No implementation necessary
}
@Override
// called every time the tab changed, whether by swiping or clicking
public void onPageSelected(int position) {
Context context = MainActivity.this;
ActionBarController controller = getActionBarView().getController();
if (mCurrentTabPosition == COMMENTS_POS) mCommentsFragment.cleanupActionBar(context, controller);
if (mCurrentTabPosition == ARTICLE_POS) mWebFragment.cleanupActionBar(context, controller);
if (position == COMMENTS_POS) mCommentsFragment.setupActionBar(context, controller);
if (position == ARTICLE_POS) mWebFragment.setupActionBar(context, controller);
View child = mTabManager.getChildAt(position);
mTabManager.protectedCheck(child.getId());
getActionBarView().hideOverflow();
mCurrentTabPosition = position;
}
};
/**
* Used to add a back pressed listener that fires AFTER overlay manager gets a chance to handle back presses in
* ActionBarActivity.
**/
private OnBackPressedListener mBackListener = new OnBackPressedListener() {
@Override
public boolean onBackPressed() {
if (mActivityType == ActivityType.COMMENTS) {
Intent intent = new Intent(MainActivity.this, MainActivity.class);
intent.putExtra(PAGE, mPage);
startActivity(intent);
// Start a new MainActivity in StoryMode. We're faking the animations to look like the app is going back
// Retrieve the animations set in the theme applied to this activity in the manifest.
TypedArray activityStyle = null;
if (getTheme() != null) {
activityStyle = getTheme().obtainStyledAttributes(new int[] { android.R.attr.windowAnimationStyle });
}
int windowAnimationStyleResId = 0;
if (activityStyle != null) {
windowAnimationStyleResId = activityStyle.getResourceId(0, 0);
activityStyle.recycle();
}
// Now retrieve the resource ids of the actual animations used in the animation style pointed to by
// the window animation resource id.
activityStyle = getTheme().obtainStyledAttributes(windowAnimationStyleResId,
new int[] { android.R.attr.activityCloseEnterAnimation, android.R.attr.activityCloseExitAnimation });
int enterAnim = 0;
int exitAnim = 0;
if (activityStyle != null) {
enterAnim = activityStyle.getResourceId(0, 0);
exitAnim = activityStyle.getResourceId(1, 0);
activityStyle.recycle();
}
if (enterAnim != 0 && exitAnim != 0) overridePendingTransition(enterAnim, exitAnim);
finish();
return true;
} else return false;
}
};
private View.OnClickListener mUpListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
MainActivity.this.onBackPressed();
}
};
private View.OnLongClickListener mUpLongListener = new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
toggle();
return true;
}
};
// Public methods
@Override
public void createActionBarController() {
// determine what kind of device we're on & which layout to show (width > 5 inches)
PointF size = Utils.pixelsToInches(this, Utils.getScreenSize(this));
if (size.x >= 5.0) {
mActivityType = ActivityType.TABLET;
getActionBarView().setController(new TwoPaneController(this, getActionBarView()));
} else {
super.createActionBarController(); // create the default OnePaneController
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Default values for custom keys
Crashlytics.start(this);
Crashlytics.setString("MainActivity :: mPage", mPage.toString());
super.addOnBackPressedListener(mBackListener);
retrieveUserData();
retrieveBundles(savedInstanceState, getIntent().getExtras());
mSharePopup = new SharePopup(this, null, getOverlayManager());
setupSearchAndTextSettingsOverflow();
retrieveFragmentsByTag();
updateUserCookie();
// determine ActivityType and setup appropriately
if (mActivityType == ActivityType.TABLET) { // two-pane
setupTabletLayout(savedInstanceState);
} else {
if (mStory != null) { // CommentsActivity
mActivityType = ActivityType.COMMENTS;
setupCommentsLayout(savedInstanceState);
if (mStoryFragment != null) mStoryFragment.onDestroy();
} else { // StoryActivity
mActivityType = ActivityType.STORY;
setupStoryLayout(savedInstanceState);
}
}
}
private void updateUserCookie() {
AsyncLoginUpdater task = new AsyncLoginUpdater(getApplicationContext());
task.execute();
}
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putSerializable(PAGE, mPage);
outState.putSerializable(STORY, mStory);
outState.putSerializable(INITIAL_TAB_POSITION, mInitialTabPosition);
super.onSaveInstanceState(outState);
}
// private methods
private void setupTabletLayout(Bundle savedInstanceState) {
setContentView(R.layout.act_tablet);
setupStoryFragment(savedInstanceState);
setupCommentsViewPager(savedInstanceState);
// enable the sliding menu swipe only in the margin
SlidingMenu menu = super.getSlidingMenu();
menu.setTouchModeAbove(SlidingMenu.TOUCHMODE_MARGIN);
}
private void setupStoryLayout(Bundle savedInstanceState) {
setContentView(R.layout.act_storylist);
setupStoryFragment(savedInstanceState);
// enable sliding menu swipe from fullscreen
SlidingMenu menu = super.getSlidingMenu();
menu.setTouchModeAbove(SlidingMenu.TOUCHMODE_FULLSCREEN);
}
private void setupCommentsLayout(Bundle savedInstanceState) {
if (mIsJobsPost && mOpenInBrowser) {
// send it straight to browser
startActivity(new Intent(Intent.ACTION_VIEW).setData(Uri.parse(mStory.url)));
}
super.setActiveMenuItem(-1);
setContentView(R.layout.act_comments);
setupCommentsViewPager(savedInstanceState);
if (mIsJobsPost) {
// ensure WebFragment is shown
mPageListener.onPageSelected(ARTICLE_POS);
mCommentsViewPager.setCurrentItem(ARTICLE_POS);
}
// 'Up' version of ActionBar
ActionBarView ab = getActionBarView();
IconView upIndicator = ab.getUpIndicator();
upIndicator.iconSource(R.drawable.ic_actionup_back);
View upButton = ab.getUpButton();
upButton.setOnClickListener(mUpListener);
upButton.setOnLongClickListener(mUpLongListener);
// disable sliding menu touchmode from the margin
SlidingMenu menu = super.getSlidingMenu();
menu.setTouchModeAbove(SlidingMenu.TOUCHMODE_NONE);
}
/** Creates the StoryFragment, attaches it to the Activity, and sets up the ActionBar. **/
private void setupStoryFragment(Bundle savedInstanceState) {
// setup story fragment
if (mStoryFragment == null) {
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
mStoryFragment = StoryFragment.newInstance(mPage);
ft.replace(R.id.frg_storylist, mStoryFragment, STORY_FRAG_TAG);
ft.disallowAddToBackStack();
ft.commit();
}
mStoryFragment.setPage(mPage);
ActionBarController controller = getActionBarView().getController();
// if it's a TwoPaneController, set the left side to be active while StoryFragment is setting up
if (controller instanceof TwoPaneController) {
TwoPaneController twoPane = (TwoPaneController) controller;
twoPane.setActiveSide(Side.LEFT);
mStoryFragment.setupActionBar(this, twoPane);
setupStoryTitleAndSlideoutMenuState();
twoPane.setActiveSide(Side.RIGHT);
} else {
mStoryFragment.setupActionBar(this, controller);
setupStoryTitleAndSlideoutMenuState();
}
}
/** Sets the ActionBar title & the checked menu item in the sliding menu based on the value of mPage. **/
private void setupStoryTitleAndSlideoutMenuState() {
// set SlidingMenu to display correct Page
int titleResId = -1;
int menuItemResId = -1;
switch (mPage) {
case FRONT:
titleResId = R.string.front_page;
menuItemResId = R.id.front_page_button;
break;
case ASK:
titleResId = R.string.ask;
menuItemResId = R.id.ask_button;
break;
case BEST:
titleResId = R.string.best;
menuItemResId = R.id.best_button;
break;
case ACTIVE:
titleResId = R.string.active;
menuItemResId = R.id.active_button;
break;
case NEW:
titleResId = R.string.new_page;
menuItemResId = R.id.new_button;
break;
default:
break;
}
ActionBarController controller = getActionBarView().getController();
controller.setTitleText(getString(titleResId));
setActiveMenuItem(menuItemResId);
}
/**
* Find the ViewPager for Comments & WebFragment, attach the adapter, create the fragments if necessary, and
* create the tabs for switching between them.
**/
private void setupCommentsViewPager(Bundle savedInstanceState) {
// setup ViewPager
mCommentsViewPager = (DisableableViewPager) findViewById(VIEWPAGER_ID);
mCommentsViewPager.setOnPageChangeListener(mPageListener);
mCommentsViewPagerAdapter = new FragmentPagerArrayAdapter(getSupportFragmentManager());
mCommentsViewPager.setAdapter(mCommentsViewPagerAdapter);
// add Comments & Web Fragments
setupCommentsFragment(savedInstanceState);
setupWebFragment(savedInstanceState);
setupCommentsTabs();
refreshTabsVisibility();
}
private void setupCommentsFragment(Bundle savedInstanceState) {
if (mCommentsFragment == null) {
mCommentsFragment = (CommentsFragment) Fragment.instantiate(this, CommentsFragment.class.getName());
}
if (mStory != null) mCommentsFragment.setStory(mStory);
mCommentsViewPagerAdapter.addItem(mCommentsFragment);
mCommentsViewPagerAdapter.notifyDataSetChanged();
}
private void setupWebFragment(Bundle savedInstanceState) {
if (mWebFragment == null) {
mWebFragment = (WebFragment) Fragment.instantiate(this, WebFragment.class.getName());
}
if (mStory != null) mWebFragment.setUrl(mStory.url);
mCommentsViewPagerAdapter.addItem(mWebFragment);
mCommentsViewPagerAdapter.notifyDataSetChanged();
}
private void setupCommentsTabs() {
// inflate tab layout & add to ActionBar
ActionBarController controller = getActionBarView().getController();
ViewGroup titleGroup = controller.getTitleGroup();
mTabs = (ViewGroup) getLayoutInflater().inflate(R.layout.vw_actionbar_tabs_comments, titleGroup, false);
titleGroup.addView(mTabs);
// setup tab manager
mTabManager = new CheckableViewManager();
for (View tab : ViewUtils.directChildViews(mTabs)) {
mTabManager.register((CheckableView) tab);
}
mTabManager.setOnCheckedChangedListener(mTabListener);
// make sure the correct tabs is checked
int position = mInitialTabPosition == CommentsTab.COMMENTS || mOpenInBrowser ? COMMENTS_POS : ARTICLE_POS;
mPageListener.onPageSelected(position);
mCommentsViewPager.setCurrentItem(position);
}
private void setupSearchAndTextSettingsOverflow() {
ActionBarController controller = getActionBarView().getController();
// setup search box
if (mSearch == null) mSearch = new SearchOverflow(this, this);
controller.addOverflowView(mSearch);
// setup text settings
if (mTextSettings == null) mTextSettings = new TextSettingsOverflow(this, this);
controller.addOverflowView(mTextSettings);
mTextSettings.refreshVisibility();
}
/**
* Shows / hides the ActionBar tabs for CommentsFragment / WebFragment based on the current state. Also enables /
* disables paging.
**/
private void refreshTabsVisibility() {
boolean tabsEnabled = mStory != null && StringUtils.isNotBlank(mStory.url) && !mOpenInBrowser && !mIsJobsPost;
mTabs.setVisibility(tabsEnabled ? View.VISIBLE : View.GONE);
mCommentsViewPager.setPagingEnabled(tabsEnabled);
}
/** Retrieves data from the Extras or savedInstanceState bundles. **/
private void retrieveBundles(Bundle savedInstanceState, Bundle extras) {
Bundle bundle = null;
if (savedInstanceState != null) bundle = savedInstanceState;
else if (extras != null) bundle = extras;
if (bundle != null) {
mPage = (Page) bundle.getSerializable(PAGE);
setStory((Story) bundle.getSerializable(STORY));
mInitialTabPosition = (CommentsTab) bundle.getSerializable(INITIAL_TAB_POSITION);
}
// default values
if (mInitialTabPosition == null) mInitialTabPosition = CommentsTab.COMMENTS;
if (mPage == null) mPage = Page.FRONT;
Crashlytics.setString("MainActivity :: mPage", mPage.toString());
}
/** Get data from UserData shared preferences. **/
private void retrieveUserData() {
UserPrefs userData = new UserPrefs(this);
// check if we should open stories in the browser or in the app
mOpenInBrowser = userData.getOpenInBrowser();
}
/** Sets the Story to display in CommentsFragment & WebFragment **/
private void setStory(Story story) {
mStory = story;
mIsJobsPost = Story.isYCombinatorJobPost(mStory);
}
/**
* Try to retrieve fragments by tag, will be null if not found (and thus will get created by their respective
* "setup" methods
**/
private void retrieveFragmentsByTag() {
FragmentManager fm = getSupportFragmentManager();
mStoryFragment = (StoryFragment) fm.findFragmentByTag(STORY_FRAG_TAG);
mCommentsFragment = (CommentsFragment) fm.findFragmentByTag(COMMENTS_FRAG_TAG);
mWebFragment = (WebFragment) fm.findFragmentByTag(WEB_FRAG_TAG);
}
// SharePopup interface
@Override
public SharePopup getSharePopup() {
return mSharePopup;
}
// TabletLayout interface
@Override
public boolean isTabletLayout() {
return mActivityType == ActivityType.TABLET;
}
// CommentsFragment.Callbacks interface
@Override
/** Receive notification when CommentsActivity has received a new Story from it's loader
* @param story - the story to load. Cannot be null.**/
public void receivedStory(Story story) {
setStory(story);
if (mActivityType == ActivityType.TABLET) {
// load new URL in webFragment
if (!mOpenInBrowser && StringUtils.isNotBlank(mStory.url)) {
mWebFragment.loadNewUrl(mStory.url);
}
// set active story in StoryAdapter
StoryAdapter adapter = mStoryFragment.getStoryAdapter();
adapter.setActiveStory(mStory);
}
}
@Override
public boolean commentsFragmentIsInLayout() {
return mActivityType != ActivityType.STORY;
}
// StoryFragment.Callbacks interface
@Override
public void showCommentsForStory(Story story, CommentsTab initialTab) {
if (mActivityType == ActivityType.TABLET) {
setStory(story);
// send new data to fragments
if (!mIsJobsPost) mCommentsFragment.loadNewStory(story);
if (StringUtils.isNotBlank(mStory.url)) mWebFragment.loadNewUrl(mStory.url);
// refresh whether tabs are visible
refreshTabsVisibility();
// make sure the correct fragment is shown
int currentFragment = (initialTab == CommentsTab.COMMENTS && !mIsJobsPost) ? COMMENTS_POS : ARTICLE_POS;
mCommentsViewPager.setCurrentItem(currentFragment);
} else {
// Don't use startCommentsActivity. It causes an animation we don't want because it sets
// Intent.FLAG_ACTIVITY_CLEAR_TOP.
// We don't need that flag because we can finish the MainActivity from here.
Intent intent = getCommentsIntent(this, mPage, story, initialTab);
startActivity(intent);
finish();
}
}
@Override
public Story getActiveStory() {
if (mCommentsFragment != null) return mCommentsFragment.getStory();
else return null;
}
@Override
public boolean storyFragmentIsInLayout() {
return mActivityType != ActivityType.COMMENTS;
}
// Static startup methods for MainActivity with Comments layout.
/** Starts a new MainActivity with data for showing a CommentsFragment. **/
public static void startCommentsActivity(Context context, Page page, long storyId, String url, CommentsTab initialTab) {
Story story = new Story();
story.storyId = storyId;
story.url = url;
startCommentsActivity(context, page, story, initialTab);
}
/** Starts a new MainActivity with data for showing a CommentsFragment. **/
public static void startCommentsActivity(Context context, Page page, Story story, CommentsTab initialTab) {
Intent intent = getCommentsIntent(context, page, story, initialTab);
// whenever you go back to the MainActivity, make sure that it's at the top of the Activity stack
// this prevents the user from going through a circular path of Activities (which will eventually crash the app)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
context.startActivity(intent);
// prevent this from going on the back stack
if (context instanceof Activity) ((Activity) context).finish();
}
/** Returns an intent for starting a new MainActivity with data for showing a CommentsFragment. **/
public static Intent getCommentsIntent(Context context, Page page, Story story, CommentsTab initialTab) {
Intent intent = new Intent(context, MainActivity.class);
intent.putExtra(MainActivity.INITIAL_TAB_POSITION, initialTab);
intent.putExtra(MainActivity.STORY, story);
intent.putExtra(MainActivity.PAGE, page);
return intent;
}
}