package com.manuelmaly.hn;
import com.manuelmaly.hn.model.HNFeed;
import com.manuelmaly.hn.model.HNPost;
import com.manuelmaly.hn.parser.BaseHTMLParser;
import com.manuelmaly.hn.server.HNCredentials;
import com.manuelmaly.hn.task.HNFeedTaskLoadMore;
import com.manuelmaly.hn.task.HNFeedTaskMainFeed;
import com.manuelmaly.hn.task.HNVoteTask;
import com.manuelmaly.hn.task.ITaskFinishedHandler;
import com.manuelmaly.hn.util.FileUtil;
import com.manuelmaly.hn.util.FontHelper;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Background;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.SystemService;
import org.androidannotations.annotations.ViewById;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.view.MenuItemCompat;
import android.support.v4.widget.SwipeRefreshLayout;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@EActivity(R.layout.main)
public class MainActivity extends BaseListActivity implements
ITaskFinishedHandler<HNFeed> {
@ViewById(R.id.main_list)
ListView mPostsList;
@ViewById(R.id.main_root)
LinearLayout mRootView;
@ViewById(R.id.main_swiperefreshlayout)
SwipeRefreshLayout mSwipeRefreshLayout;
@SystemService
LayoutInflater mInflater;
TextView mEmptyListPlaceholder;
HNFeed mFeed;
PostsAdapter mPostsListAdapter;
Set<HNPost> mUpvotedPosts;
Set<Integer> mAlreadyRead;
String mCurrentFontSize = null;
int mFontSizeTitle;
int mFontSizeDetails;
int mTitleColor;
int mTitleReadColor;
private static final int TASKCODE_LOAD_FEED = 10;
private static final int TASKCODE_LOAD_MORE_POSTS = 20;
private static final int TASKCODE_VOTE = 100;
private static final String LIST_STATE = "listState";
private static final String ALREADY_READ_ARTICLES_KEY = "HN_ALREADY_READ";
private Parcelable mListState = null;
boolean mShouldShowRefreshing = false;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Make sure that we show the overflow menu icon
try {
ViewConfiguration config = ViewConfiguration.get(this);
Field menuKeyField = ViewConfiguration.class
.getDeclaredField("sHasPermanentMenuKey");
if (menuKeyField != null) {
menuKeyField.setAccessible(true);
menuKeyField.setBoolean(config, false);
}
} catch (Exception e) {
// presumably, not relevant
}
TextView tv = (TextView) getSupportActionBar().getCustomView()
.findViewById(R.id.actionbar_title);
tv.setTypeface(FontHelper.getComfortaa(this, true));
}
@AfterViews
public void init() {
mFeed = new HNFeed(new ArrayList<HNPost>(), null, "");
mPostsListAdapter = new PostsAdapter();
mUpvotedPosts = new HashSet<HNPost>();
mEmptyListPlaceholder = getEmptyTextView(mRootView);
mPostsList.setEmptyView(mEmptyListPlaceholder);
mPostsList.setAdapter(mPostsListAdapter);
mEmptyListPlaceholder.setTypeface(FontHelper.getComfortaa(this, true));
mTitleColor = getResources().getColor(R.color.dark_gray_post_title);
mTitleReadColor = getResources().getColor(R.color.gray_post_title_read);
toggleSwipeRefreshLayout();
mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
startFeedLoading();
}
});
loadAlreadyReadCache();
loadIntermediateFeedFromStore();
startFeedLoading();
}
@Override
protected void onResume() {
super.onResume();
boolean registeredUserChanged = mFeed.getUserAcquiredFor() != null
&& (!mFeed.getUserAcquiredFor().equals(
Settings.getUserName(this)));
// We want to reload the feed if a new user logged in
if (HNCredentials.isInvalidated() || registeredUserChanged) {
showFeed(new HNFeed(new ArrayList<HNPost>(), null, ""));
startFeedLoading();
}
// refresh if font size changed
if (refreshFontSizes()) {
mPostsListAdapter.notifyDataSetChanged();
}
// restore vertical scrolling position if applicable
if (mListState != null) {
mPostsList.onRestoreInstanceState(mListState);
}
mListState = null;
// User may have toggled pull-down refresh, so toggle the SwipeRefreshLayout.
toggleSwipeRefreshLayout();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem item = menu.findItem(R.id.menu_refresh);
if (!mShouldShowRefreshing) {
MenuItemCompat.setActionView(item, null);
} else {
View v = mInflater.inflate(R.layout.refresh_icon, null);
MenuItemCompat.setActionView(item, v);
}
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_settings:
startActivity(new Intent(MainActivity.this, SettingsActivity.class));
return true;
case R.id.menu_about:
startActivity(new Intent(MainActivity.this, AboutActivity_.class));
return true;
case R.id.menu_refresh:
startFeedLoading();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void toggleSwipeRefreshLayout() {
mSwipeRefreshLayout.setEnabled(Settings.isPullDownRefresh(MainActivity.this));
}
@Override
public void onTaskFinished(int taskCode, TaskResultCode code,
HNFeed result, Object tag) {
if (taskCode == TASKCODE_LOAD_FEED) {
if (code.equals(TaskResultCode.Success)
&& mPostsListAdapter != null) {
showFeed(result);
} else
if (!code.equals(TaskResultCode.Success)) {
Toast.makeText(this,
getString(R.string.error_unable_to_retrieve_feed),
Toast.LENGTH_SHORT).show();
}
} else
if (taskCode == TASKCODE_LOAD_MORE_POSTS) {
if (!code.equals(TaskResultCode.Success) || result == null || result.getPosts() == null || result.getPosts().size() == 0) {
Toast.makeText(this,
getString(R.string.error_unable_to_load_more),
Toast.LENGTH_SHORT).show();
mFeed.setLoadedMore(true); // reached the end.
}
mFeed.appendLoadMoreFeed(result);
mPostsListAdapter.notifyDataSetChanged();
}
setShowRefreshing(false);
}
@Background
void loadAlreadyReadCache() {
if (mAlreadyRead == null) {
mAlreadyRead = new HashSet<Integer>();
}
SharedPreferences sharedPref = getSharedPreferences(
ALREADY_READ_ARTICLES_KEY, Context.MODE_PRIVATE);
Editor editor = sharedPref.edit();
Map<String, ?> read = sharedPref.getAll();
Long now = new Date().getTime();
for (Map.Entry<String, ?> entry : read.entrySet()) {
Long readAt = (Long) entry.getValue();
Long diff = (now - readAt) / (24 * 60 * 60 * 1000);
if (diff >= 2) {
editor.remove(entry.getKey());
} else {
mAlreadyRead.add(entry.getKey().hashCode());
}
}
editor.commit();
}
@Background
void markAsRead(HNPost post) {
Long now = new Date().getTime();
String title = post.getTitle();
Editor editor = getSharedPreferences(ALREADY_READ_ARTICLES_KEY,
Context.MODE_PRIVATE).edit();
editor.putLong(title, now);
editor.commit();
mAlreadyRead.add(title.hashCode());
}
private void showFeed(HNFeed feed) {
mFeed = feed;
mPostsListAdapter.notifyDataSetChanged();
}
private void loadIntermediateFeedFromStore() {
new GetLastHNFeedTask().execute((Void) null);
long start = System.currentTimeMillis();
Log.i("",
"Loading intermediate feed took ms:"
+ (System.currentTimeMillis() - start));
}
class GetLastHNFeedTask extends FileUtil.GetLastHNFeedTask {
ProgressDialog progress;
@Override
protected void onPreExecute() {
progress = new ProgressDialog(MainActivity.this);
progress.setMessage("Loading");
progress.show();
}
@Override
protected void onPostExecute(HNFeed result) {
if (progress != null && progress.isShowing()) {
progress.dismiss();
}
if (result != null
&& result.getUserAcquiredFor() != null
&& result.getUserAcquiredFor().equals(
Settings.getUserName(App.getInstance()))) {
showFeed(result);
}
}
}
private void startFeedLoading() {
setShowRefreshing(true);
HNFeedTaskMainFeed.startOrReattach(this, this, TASKCODE_LOAD_FEED);
}
private boolean refreshFontSizes() {
final String fontSize = Settings.getFontSize(this);
if ((mCurrentFontSize == null) || (!mCurrentFontSize.equals(fontSize))) {
mCurrentFontSize = fontSize;
if (fontSize.equals(getString(R.string.pref_fontsize_small))) {
mFontSizeTitle = 15;
mFontSizeDetails = 11;
} else
if (fontSize.equals(getString(R.string.pref_fontsize_normal))) {
mFontSizeTitle = 18;
mFontSizeDetails = 12;
} else {
mFontSizeTitle = 22;
mFontSizeDetails = 15;
}
return true;
} else {
return false;
}
}
private void vote(String voteURL, HNPost post) {
HNVoteTask.start(voteURL, MainActivity.this,
new VoteTaskFinishedHandler(), TASKCODE_VOTE, post);
}
@Override
protected void onRestoreInstanceState(Bundle state) {
super.onRestoreInstanceState(state);
mListState = state.getParcelable(LIST_STATE);
}
@Override
protected void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
mListState = mPostsList.onSaveInstanceState();
state.putParcelable(LIST_STATE, mListState);
}
class VoteTaskFinishedHandler implements ITaskFinishedHandler<Boolean> {
@Override
public void onTaskFinished(
int taskCode,
com.manuelmaly.hn.task.ITaskFinishedHandler.TaskResultCode code,
Boolean result, Object tag) {
if (taskCode == TASKCODE_VOTE) {
if (result != null && result.booleanValue()) {
Toast.makeText(MainActivity.this, R.string.vote_success,
Toast.LENGTH_SHORT).show();
HNPost post = (HNPost) tag;
if (post != null) {
mUpvotedPosts.add(post);
}
} else {
Toast.makeText(MainActivity.this, R.string.vote_error,
Toast.LENGTH_LONG).show();
}
}
}
}
class PostsAdapter extends BaseAdapter {
private static final int VIEWTYPE_POST = 0;
private static final int VIEWTYPE_LOADMORE = 1;
private static final String HACKERNEWS_URLDOMAIN = "news.ycombinator.com";
@Override
public int getCount() {
int posts = mFeed.getPosts().size();
if (posts == 0) {
return 0;
} else {
return posts + (mFeed.isLoadedMore() ? 0 : 1);
}
}
@Override
public HNPost getItem(int position) {
if (getItemViewType(position) == VIEWTYPE_POST) {
return mFeed.getPosts().get(position);
} else {
return null;
}
}
@Override
public long getItemId(int position) {
// Item ID not needed here:
return 0;
}
@Override
public int getItemViewType(int position) {
if (position < mFeed.getPosts().size()) {
return VIEWTYPE_POST;
} else {
return VIEWTYPE_LOADMORE;
}
}
@Override
public int getViewTypeCount() {
return 2;
}
@Override
public View getView(final int position, View convertView,
ViewGroup parent) {
switch (getItemViewType(position)) {
case VIEWTYPE_POST:
if (convertView == null) {
convertView = mInflater.inflate(R.layout.main_list_item,
null);
PostViewHolder holder = new PostViewHolder();
holder.titleView = (TextView) convertView
.findViewById(R.id.main_list_item_title);
holder.urlView = (TextView) convertView
.findViewById(R.id.main_list_item_url);
holder.textContainer = (LinearLayout) convertView
.findViewById(R.id.main_list_item_textcontainer);
holder.commentsButton = (Button) convertView
.findViewById(R.id.main_list_item_comments_button);
holder.commentsButton.setTypeface(FontHelper.getComfortaa(
MainActivity.this, false));
holder.pointsView = (TextView) convertView
.findViewById(R.id.main_list_item_points);
holder.pointsView.setTypeface(FontHelper.getComfortaa(
MainActivity.this, true));
convertView.setTag(holder);
}
final HNPost item = getItem(position);
PostViewHolder holder = (PostViewHolder) convertView.getTag();
holder.titleView.setTextSize(TypedValue.COMPLEX_UNIT_DIP,
mFontSizeTitle);
holder.titleView.setText(item.getTitle());
holder.titleView.setTextColor(isRead(item) ? mTitleReadColor
: mTitleColor);
holder.urlView.setTextSize(TypedValue.COMPLEX_UNIT_DIP,
mFontSizeDetails);
holder.urlView.setText(item.getURLDomain());
holder.pointsView.setTextSize(TypedValue.COMPLEX_UNIT_DIP,
mFontSizeDetails);
if (item.getPoints() != BaseHTMLParser.UNDEFINED) {
holder.pointsView.setText(item.getPoints() + "");
} else {
holder.pointsView.setText("-");
}
holder.commentsButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP,
mFontSizeTitle);
if (item.getCommentsCount() != BaseHTMLParser.UNDEFINED) {
holder.commentsButton.setVisibility(View.VISIBLE);
holder.commentsButton.setText(item.getCommentsCount() + "");
} else {
holder.commentsButton.setVisibility(View.INVISIBLE);
}
holder.commentsButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startCommentActivity(position);
}
});
holder.textContainer.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
markAsRead(item);
if(getItem(position).getURLDomain().equals(HACKERNEWS_URLDOMAIN)){
startCommentActivity(position);
}
else if (Settings.getHtmlViewer(MainActivity.this).equals(
getString(R.string.pref_htmlviewer_browser))) {
openURLInBrowser(
getArticleViewURL(getItem(position)),
MainActivity.this);
} else {
openPostInApp(getItem(position), null,
MainActivity.this);
}
}
});
holder.textContainer
.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
final HNPost post = getItem(position);
AlertDialog.Builder builder = new AlertDialog.Builder(
MainActivity.this);
LongPressMenuListAdapter adapter = new LongPressMenuListAdapter(
post);
builder.setAdapter(adapter, adapter).show();
return true;
}
});
break;
case VIEWTYPE_LOADMORE:
// I don't use the preloaded convertView here because it's
// only one cell
convertView = mInflater.inflate(
R.layout.main_list_item_loadmore, null);
final TextView textView = (TextView) convertView
.findViewById(R.id.main_list_item_loadmore_text);
textView.setTypeface(FontHelper.getComfortaa(MainActivity.this,
true));
final ImageView imageView = (ImageView) convertView
.findViewById(R.id.main_list_item_loadmore_loadingimage);
if (HNFeedTaskLoadMore.isRunning(MainActivity.this,
TASKCODE_LOAD_MORE_POSTS)) {
textView.setVisibility(View.INVISIBLE);
imageView.setVisibility(View.VISIBLE);
convertView.setClickable(false);
}
final View convertViewFinal = convertView;
convertView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
textView.setVisibility(View.INVISIBLE);
imageView.setVisibility(View.VISIBLE);
convertViewFinal.setClickable(false);
HNFeedTaskLoadMore.start(MainActivity.this,
MainActivity.this, mFeed,
TASKCODE_LOAD_MORE_POSTS);
setShowRefreshing(true);
}
});
break;
default:
break;
}
return convertView;
}
private boolean isRead(HNPost post) {
return mAlreadyRead.contains(post.getTitle().hashCode());
}
private void startCommentActivity(int position){
Intent i = new Intent(MainActivity.this,
CommentsActivity_.class);
i.putExtra(CommentsActivity.EXTRA_HNPOST,
getItem(position));
startActivity(i);
}
}
private class LongPressMenuListAdapter implements ListAdapter,
DialogInterface.OnClickListener {
HNPost mPost;
boolean mIsLoggedIn;
boolean mUpVotingEnabled;
ArrayList<CharSequence> mItems;
public LongPressMenuListAdapter(HNPost post) {
mPost = post;
mIsLoggedIn = Settings.isUserLoggedIn(MainActivity.this);
mUpVotingEnabled = !mIsLoggedIn
|| (mPost.getUpvoteURL(Settings
.getUserName(MainActivity.this)) != null && !mUpvotedPosts
.contains(mPost));
mItems = new ArrayList<CharSequence>();
if (mUpVotingEnabled) {
mItems.add(getString(R.string.upvote));
} else {
mItems.add(getString(R.string.already_upvoted));
}
mItems.addAll(Arrays.asList(
getString(R.string.pref_htmlprovider_original_url),
getString(R.string.pref_htmlprovider_viewtext),
getString(R.string.pref_htmlprovider_google),
getString(R.string.pref_htmlprovider_instapaper),
getString(R.string.external_browser),
getString(R.string.share_article_url)));
}
@Override
public int getCount() {
return mItems.size();
}
@Override
public CharSequence getItem(int position) {
return mItems.get(position);
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public int getItemViewType(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView view = (TextView) mInflater.inflate(
android.R.layout.simple_list_item_1, null);
view.setText(getItem(position));
if (!mUpVotingEnabled && position == 0) {
view.setTextColor(getResources().getColor(
android.R.color.darker_gray));
}
return view;
}
@Override
public int getViewTypeCount() {
return 1;
}
@Override
public boolean hasStableIds() {
return false;
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public void registerDataSetObserver(DataSetObserver observer) {
}
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
}
@Override
public boolean areAllItemsEnabled() {
return false;
}
@Override
public boolean isEnabled(int position) {
if (!mUpVotingEnabled && position == 4) {
return false;
}
return true;
}
@Override
public void onClick(DialogInterface dialog, int item) {
switch (item) {
case 0:
if (!mIsLoggedIn) {
Toast.makeText(MainActivity.this, R.string.please_log_in,
Toast.LENGTH_LONG).show();
} else
if (mUpVotingEnabled) {
vote(mPost.getUpvoteURL(Settings
.getUserName(MainActivity.this)), mPost);
}
break;
case 1:
case 2:
case 3:
case 4:
openPostInApp(mPost, getItem(item).toString(),
MainActivity.this);
markAsRead(mPost);
break;
case 5:
openURLInBrowser(getArticleViewURL(mPost), MainActivity.this);
markAsRead(mPost);
break;
case 6:
shareUrl(mPost, MainActivity.this);
break;
default:
break;
}
}
}
private String getArticleViewURL(HNPost post) {
return ArticleReaderActivity.getArticleViewURL(post,
Settings.getHtmlProvider(this), this);
}
public static void openURLInBrowser(String url, Activity a) {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
a.startActivity(browserIntent);
}
public static void openPostInApp(HNPost post, String overrideHtmlProvider,
Activity a) {
Intent i = new Intent(a, ArticleReaderActivity_.class);
i.putExtra(ArticleReaderActivity.EXTRA_HNPOST, post);
if (overrideHtmlProvider != null) {
i.putExtra(ArticleReaderActivity.EXTRA_HTMLPROVIDER_OVERRIDE,
overrideHtmlProvider);
}
a.startActivity(i);
}
public static void shareUrl(HNPost post, Activity a){
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_SUBJECT, post.getTitle());
shareIntent.putExtra(Intent.EXTRA_TEXT, post.getURL());
a.startActivity(Intent.createChooser(shareIntent, a.getString(R.string.share_article_url)));
}
private void setShowRefreshing(boolean showRefreshing) {
if (!Settings.isPullDownRefresh(MainActivity.this)) {
mShouldShowRefreshing = showRefreshing;
supportInvalidateOptionsMenu();
}
if (mSwipeRefreshLayout.isEnabled() && (!mSwipeRefreshLayout.isRefreshing() || !showRefreshing)) {
mSwipeRefreshLayout.setRefreshing(showRefreshing);
}
}
static class PostViewHolder {
TextView titleView;
TextView urlView;
TextView pointsView;
TextView commentsCountView;
LinearLayout textContainer;
Button commentsButton;
}
}