├── .gitignore
├── AndroidManifest.xml
├── README.md
├── gen
└── com
│ └── nerdability
│ └── android
│ ├── BuildConfig.java
│ └── R.java
├── ic_launcher-web.png
├── project.properties
├── res
├── drawable-hdpi
│ ├── offline.png
│ ├── refresh.png
│ ├── rssicon.png
│ ├── starred.png
│ ├── unread.png
│ └── unstarred.png
├── drawable-ldpi
│ ├── offline.png
│ ├── refresh.png
│ ├── rssicon.png
│ ├── starred.png
│ ├── unread.png
│ └── unstarred.png
├── drawable-mdpi
│ ├── offline.png
│ ├── refresh.png
│ ├── rssicon.png
│ ├── starred.png
│ ├── unread.png
│ └── unstarred.png
├── drawable-xhdpi
│ ├── offline.png
│ ├── refresh.png
│ ├── rssicon.png
│ ├── starred.png
│ ├── unread.png
│ └── unstarred.png
├── layout
│ ├── activity_article_detail.xml
│ ├── activity_article_list.xml
│ ├── activity_article_twopane.xml
│ ├── fragment_article_detail.xml
│ └── fragment_article_list.xml
├── menu
│ ├── detailmenu.xml
│ └── refreshmenu.xml
├── values-large
│ └── refs.xml
├── values-sw600dp
│ └── refs.xml
├── values-v11
│ └── styles.xml
├── values-v14
│ └── styles.xml
└── values
│ ├── strings.xml
│ └── styles.xml
└── src
└── com
└── nerdability
└── android
├── ArticleDetailActivity.java
├── ArticleDetailFragment.java
├── ArticleListActivity.java
├── ArticleListFragment.java
├── adapter
└── ArticleListAdapter.java
├── db
└── DbAdapter.java
├── rss
├── RssService.java
├── domain
│ └── Article.java
└── parser
│ └── RssHandler.java
└── util
└── DateUtils.java
/.gitignore:
--------------------------------------------------------------------------------
1 | *.class
2 |
3 | # Package Files #
4 | *.jar
5 | *.war
6 | *.ear
7 | .project
8 | .classpath
9 | bin
--------------------------------------------------------------------------------
/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
9 |
11 |
12 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
AndroidRssReader
2 |
3 | Guide: http://blog.nerdability.com/2013/03/tech-building-rss-reader-android-app.html
4 |
5 | We have built a simple RSS reader app for Android (3.0+) as part of a tutorial on parsing RSS feeds on the platform.
6 |
7 | The app is fully functioning and is ready to be forked/downloaded and run on any compatible Android device/emulator - by default it will parse the http://nerdability.com blog.
8 |
9 | It also has some other built in features such as a basic DB tracking which posts you have already read.
10 |
11 | RIP Google Reader
12 |
13 |
14 |
15 | License
16 |
17 | Copyright (c) 2013 NerdAbility Ltd.
18 |
19 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
20 |
21 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
22 |
23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 |
25 |
--------------------------------------------------------------------------------
/gen/com/nerdability/android/BuildConfig.java:
--------------------------------------------------------------------------------
1 | /** Automatically generated file. DO NOT MODIFY */
2 | package com.nerdability.android;
3 |
4 | public final class BuildConfig {
5 | public final static boolean DEBUG = true;
6 | }
--------------------------------------------------------------------------------
/gen/com/nerdability/android/R.java:
--------------------------------------------------------------------------------
1 | /* AUTO-GENERATED FILE. DO NOT MODIFY.
2 | *
3 | * This class was automatically generated by the
4 | * aapt tool from the resource data it found. It
5 | * should not be modified by hand.
6 | */
7 |
8 | package com.nerdability.android;
9 |
10 | public final class R {
11 | public static final class attr {
12 | }
13 | public static final class drawable {
14 | public static final int offline=0x7f020000;
15 | public static final int refresh=0x7f020001;
16 | public static final int rssicon=0x7f020002;
17 | public static final int starred=0x7f020003;
18 | public static final int unread=0x7f020004;
19 | public static final int unstarred=0x7f020005;
20 | }
21 | public static final class id {
22 | public static final int ScrollView=0x7f070002;
23 | public static final int actionbar_markunread=0x7f07000a;
24 | public static final int actionbar_refresh=0x7f07000c;
25 | public static final int actionbar_saveoffline=0x7f07000b;
26 | public static final int article_author=0x7f070005;
27 | public static final int article_detail=0x7f070006;
28 | public static final int article_detail_container=0x7f070000;
29 | public static final int article_detail_layout=0x7f070003;
30 | public static final int article_list=0x7f070001;
31 | public static final int article_listing_smallprint=0x7f070009;
32 | public static final int article_row_layout=0x7f070007;
33 | public static final int article_title=0x7f070004;
34 | public static final int article_title_text=0x7f070008;
35 | }
36 | public static final class layout {
37 | public static final int activity_article_detail=0x7f030000;
38 | public static final int activity_article_list=0x7f030001;
39 | public static final int activity_article_twopane=0x7f030002;
40 | public static final int fragment_article_detail=0x7f030003;
41 | public static final int fragment_article_list=0x7f030004;
42 | }
43 | public static final class menu {
44 | public static final int detailmenu=0x7f060000;
45 | public static final int refreshmenu=0x7f060001;
46 | }
47 | public static final class string {
48 | public static final int app_name=0x7f040000;
49 | public static final int btn_mark_unread=0x7f040004;
50 | public static final int btn_offline_reading=0x7f040003;
51 | public static final int btn_refresh=0x7f040005;
52 | public static final int title_article_detail=0x7f040001;
53 | public static final int title_article_list=0x7f040002;
54 | }
55 | public static final class style {
56 | public static final int AppTheme=0x7f050000;
57 | public static final int MyActionBar=0x7f050001;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/ic_launcher-web.png
--------------------------------------------------------------------------------
/project.properties:
--------------------------------------------------------------------------------
1 | # This file is automatically generated by Android Tools.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file must be checked in Version Control Systems.
5 | #
6 | # To customize properties used by the Ant build system edit
7 | # "ant.properties", and override values to adapt the script to your
8 | # project structure.
9 | #
10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
12 |
13 | # Project target.
14 | target=android-16
15 |
--------------------------------------------------------------------------------
/res/drawable-hdpi/offline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-hdpi/offline.png
--------------------------------------------------------------------------------
/res/drawable-hdpi/refresh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-hdpi/refresh.png
--------------------------------------------------------------------------------
/res/drawable-hdpi/rssicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-hdpi/rssicon.png
--------------------------------------------------------------------------------
/res/drawable-hdpi/starred.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-hdpi/starred.png
--------------------------------------------------------------------------------
/res/drawable-hdpi/unread.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-hdpi/unread.png
--------------------------------------------------------------------------------
/res/drawable-hdpi/unstarred.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-hdpi/unstarred.png
--------------------------------------------------------------------------------
/res/drawable-ldpi/offline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-ldpi/offline.png
--------------------------------------------------------------------------------
/res/drawable-ldpi/refresh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-ldpi/refresh.png
--------------------------------------------------------------------------------
/res/drawable-ldpi/rssicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-ldpi/rssicon.png
--------------------------------------------------------------------------------
/res/drawable-ldpi/starred.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-ldpi/starred.png
--------------------------------------------------------------------------------
/res/drawable-ldpi/unread.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-ldpi/unread.png
--------------------------------------------------------------------------------
/res/drawable-ldpi/unstarred.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-ldpi/unstarred.png
--------------------------------------------------------------------------------
/res/drawable-mdpi/offline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-mdpi/offline.png
--------------------------------------------------------------------------------
/res/drawable-mdpi/refresh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-mdpi/refresh.png
--------------------------------------------------------------------------------
/res/drawable-mdpi/rssicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-mdpi/rssicon.png
--------------------------------------------------------------------------------
/res/drawable-mdpi/starred.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-mdpi/starred.png
--------------------------------------------------------------------------------
/res/drawable-mdpi/unread.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-mdpi/unread.png
--------------------------------------------------------------------------------
/res/drawable-mdpi/unstarred.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-mdpi/unstarred.png
--------------------------------------------------------------------------------
/res/drawable-xhdpi/offline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-xhdpi/offline.png
--------------------------------------------------------------------------------
/res/drawable-xhdpi/refresh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-xhdpi/refresh.png
--------------------------------------------------------------------------------
/res/drawable-xhdpi/rssicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-xhdpi/rssicon.png
--------------------------------------------------------------------------------
/res/drawable-xhdpi/starred.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-xhdpi/starred.png
--------------------------------------------------------------------------------
/res/drawable-xhdpi/unread.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-xhdpi/unread.png
--------------------------------------------------------------------------------
/res/drawable-xhdpi/unstarred.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdability/AndroidRssReader/aa8acb6e81409c9e216dc681eecad3c2f2f46cdf/res/drawable-xhdpi/unstarred.png
--------------------------------------------------------------------------------
/res/layout/activity_article_detail.xml:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/res/layout/activity_article_list.xml:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/res/layout/activity_article_twopane.xml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
17 |
18 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/res/layout/fragment_article_detail.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
14 |
15 |
27 |
28 |
41 |
42 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/res/layout/fragment_article_list.xml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
23 |
24 |
25 |
37 |
38 |
--------------------------------------------------------------------------------
/res/menu/detailmenu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/res/menu/refreshmenu.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/res/values-large/refs.xml:
--------------------------------------------------------------------------------
1 |
2 | - @layout/activity_article_twopane
3 |
4 |
--------------------------------------------------------------------------------
/res/values-sw600dp/refs.xml:
--------------------------------------------------------------------------------
1 |
2 | - @layout/activity_article_twopane
3 |
4 |
--------------------------------------------------------------------------------
/res/values-v11/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/res/values-v14/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | NerdAbility Reader
3 | Post Detail
4 | NerdAbility Reader
5 | Save for later
6 | Mark unread
7 | Refresh
8 |
--------------------------------------------------------------------------------
/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/com/nerdability/android/ArticleDetailActivity.java:
--------------------------------------------------------------------------------
1 | package com.nerdability.android;
2 |
3 | import com.nerdability.android.R;
4 |
5 | import android.content.Intent;
6 | import android.os.Bundle;
7 | import android.support.v4.app.FragmentActivity;
8 | import android.support.v4.app.NavUtils;
9 | import android.view.MenuItem;
10 |
11 | public class ArticleDetailActivity extends FragmentActivity {
12 |
13 | @Override
14 | protected void onCreate(Bundle savedInstanceState) {
15 | super.onCreate(savedInstanceState);
16 | setContentView(R.layout.activity_article_detail);
17 |
18 | getActionBar().setDisplayHomeAsUpEnabled(true);
19 |
20 | if (savedInstanceState == null) {
21 | Bundle arguments = new Bundle();
22 | arguments.putString(ArticleDetailFragment.ARG_ITEM_ID,
23 | getIntent().getStringExtra(ArticleDetailFragment.ARG_ITEM_ID));
24 | ArticleDetailFragment fragment = new ArticleDetailFragment();
25 | fragment.setArguments(arguments);
26 | getSupportFragmentManager().beginTransaction()
27 | .add(R.id.article_detail_container, fragment)
28 | .commit();
29 | }
30 | }
31 |
32 | @Override
33 | public boolean onOptionsItemSelected(MenuItem item) {
34 | if (item.getItemId() == android.R.id.home) {
35 | NavUtils.navigateUpTo(this, new Intent(this, ArticleListActivity.class));
36 | return true;
37 | }
38 |
39 | return super.onOptionsItemSelected(item);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/com/nerdability/android/ArticleDetailFragment.java:
--------------------------------------------------------------------------------
1 | package com.nerdability.android;
2 |
3 | import java.text.ParseException;
4 | import java.text.SimpleDateFormat;
5 | import java.util.Date;
6 | import java.util.Locale;
7 |
8 | import android.os.Bundle;
9 | import android.support.v4.app.Fragment;
10 | import android.text.Html;
11 | import android.util.Log;
12 | import android.view.LayoutInflater;
13 | import android.view.Menu;
14 | import android.view.MenuInflater;
15 | import android.view.MenuItem;
16 | import android.view.View;
17 | import android.view.ViewGroup;
18 | import android.widget.TextView;
19 | import android.widget.Toast;
20 |
21 | import com.nerdability.android.R;
22 | import com.nerdability.android.adapter.ArticleListAdapter;
23 | import com.nerdability.android.db.DbAdapter;
24 | import com.nerdability.android.rss.domain.Article;
25 | import com.nerdability.android.util.DateUtils;
26 |
27 | public class ArticleDetailFragment extends Fragment {
28 |
29 | public static final String ARG_ITEM_ID = "item_id";
30 |
31 | Article displayedArticle;
32 | DbAdapter db;
33 |
34 | public ArticleDetailFragment() {
35 | setHasOptionsMenu(true); //this enables us to set actionbar from fragment
36 | }
37 |
38 | @Override
39 | public void onCreate(Bundle savedInstanceState) {
40 | super.onCreate(savedInstanceState);
41 | db = new DbAdapter(getActivity());
42 | if (getArguments().containsKey(Article.KEY)) {
43 | displayedArticle = (Article) getArguments().getSerializable(Article.KEY);
44 | }
45 | }
46 |
47 | @Override
48 | public View onCreateView(LayoutInflater inflater, ViewGroup container,
49 | Bundle savedInstanceState) {
50 | View rootView = inflater.inflate(R.layout.fragment_article_detail, container, false);
51 | if (displayedArticle != null) {
52 | String title = displayedArticle.getTitle();
53 | String pubDate = displayedArticle.getPubDate();
54 | SimpleDateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy kk:mm:ss Z", Locale.ENGLISH);
55 | try {
56 | Date pDate = df.parse(pubDate);
57 | pubDate = "This post was published " + DateUtils.getDateDifference(pDate) + " by " + displayedArticle.getAuthor();
58 | } catch (ParseException e) {
59 | Log.e("DATE PARSING", "Error parsing date..");
60 | pubDate = "published by " + displayedArticle.getAuthor();
61 | }
62 |
63 | String content = displayedArticle.getEncodedContent();
64 | ((TextView) rootView.findViewById(R.id.article_title)).setText(title);
65 | ((TextView) rootView.findViewById(R.id.article_author)).setText(pubDate);
66 | ((TextView) rootView.findViewById(R.id.article_detail)).setText(Html.fromHtml(content));
67 | }
68 | return rootView;
69 | }
70 |
71 |
72 | @Override
73 | public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
74 | inflater.inflate(R.menu.detailmenu, menu);
75 | }
76 |
77 | @Override
78 | public boolean onOptionsItemSelected(MenuItem item) {
79 | int id = item.getItemId();
80 | Log.d("item ID : ", "onOptionsItemSelected Item ID" + id);
81 | if (id == R.id.actionbar_saveoffline) {
82 | Toast.makeText(getActivity().getApplicationContext(), "This article has been saved of offline reading.", Toast.LENGTH_LONG).show();
83 | return true;
84 | } else if (id == R.id.actionbar_markunread) {
85 | db.openToWrite();
86 | db.markAsUnread(displayedArticle.getGuid());
87 | db.close();
88 | displayedArticle.setRead(false);
89 | ArticleListAdapter adapter = (ArticleListAdapter) ((ArticleListFragment) getActivity().getSupportFragmentManager().findFragmentById(R.id.article_list)).getListAdapter();
90 | adapter.notifyDataSetChanged();
91 | return true;
92 | } else {
93 | return super.onOptionsItemSelected(item);
94 | }
95 | }
96 | }
--------------------------------------------------------------------------------
/src/com/nerdability/android/ArticleListActivity.java:
--------------------------------------------------------------------------------
1 | package com.nerdability.android;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 | import android.support.v4.app.FragmentActivity;
6 | import android.util.Log;
7 |
8 | import com.nerdability.android.R;
9 | import com.nerdability.android.adapter.ArticleListAdapter;
10 | import com.nerdability.android.db.DbAdapter;
11 | import com.nerdability.android.rss.domain.Article;
12 |
13 | public class ArticleListActivity extends FragmentActivity implements ArticleListFragment.Callbacks {
14 |
15 | private boolean mTwoPane;
16 | private DbAdapter dba;
17 |
18 | public ArticleListActivity(){}
19 |
20 | @Override
21 | public void onCreate(Bundle savedInstanceState) {
22 | super.onCreate(savedInstanceState);
23 | setContentView(R.layout.activity_article_list);
24 | dba = new DbAdapter(this);
25 |
26 | if (findViewById(R.id.article_detail_container) != null) {
27 | mTwoPane = true;
28 | ((ArticleListFragment) getSupportFragmentManager()
29 | .findFragmentById(R.id.article_list))
30 | .setActivateOnItemClick(true);
31 | }
32 | }
33 |
34 |
35 | @Override
36 | public void onItemSelected(String id) {
37 | Article selected = (Article) ((ArticleListFragment) getSupportFragmentManager().findFragmentById(R.id.article_list)).getListAdapter().getItem(Integer.parseInt(id));
38 |
39 | //mark article as read
40 | dba.openToWrite();
41 | dba.markAsRead(selected.getGuid());
42 | dba.close();
43 | selected.setRead(true);
44 | ArticleListAdapter adapter = (ArticleListAdapter) ((ArticleListFragment) getSupportFragmentManager().findFragmentById(R.id.article_list)).getListAdapter();
45 | adapter.notifyDataSetChanged();
46 | Log.e("CHANGE", "Changing to read: ");
47 |
48 |
49 | //load article details to main panel
50 | if (mTwoPane) {
51 | Bundle arguments = new Bundle();
52 | arguments.putSerializable (Article.KEY, selected);
53 |
54 | ArticleDetailFragment fragment = new ArticleDetailFragment();
55 | fragment.setArguments(arguments);
56 | getSupportFragmentManager().beginTransaction()
57 | .replace(R.id.article_detail_container, fragment)
58 | .commit();
59 |
60 | } else {
61 | Intent detailIntent = new Intent(this, ArticleDetailActivity.class);
62 | detailIntent.putExtra(ArticleDetailFragment.ARG_ITEM_ID, id);
63 | startActivity(detailIntent);
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/com/nerdability/android/ArticleListFragment.java:
--------------------------------------------------------------------------------
1 | package com.nerdability.android;
2 |
3 | import android.app.Activity;
4 | import android.os.Bundle;
5 | import android.support.v4.app.ListFragment;
6 | import android.view.Menu;
7 | import android.view.MenuInflater;
8 | import android.view.MenuItem;
9 | import android.view.View;
10 | import android.widget.ListView;
11 |
12 | import com.nerdability.android.R;
13 | import com.nerdability.android.rss.RssService;
14 |
15 | public class ArticleListFragment extends ListFragment {
16 |
17 | private static final String STATE_ACTIVATED_POSITION = "activated_position";
18 | private static final String BLOG_URL = "http://blog.nerdability.com/feeds/posts/default";
19 | private Callbacks mCallbacks = sDummyCallbacks;
20 | private int mActivatedPosition = ListView.INVALID_POSITION;
21 | private RssService rssService;
22 |
23 | public interface Callbacks {
24 | public void onItemSelected(String id);
25 | }
26 |
27 | private static Callbacks sDummyCallbacks = new Callbacks() {
28 | @Override
29 | public void onItemSelected(String id) {
30 | }
31 | };
32 |
33 | public ArticleListFragment() {
34 | setHasOptionsMenu(true); //this enables us to set actionbar from fragment
35 | }
36 |
37 | @Override
38 | public void onCreate(Bundle savedInstanceState) {
39 | super.onCreate(savedInstanceState);
40 | refreshList();
41 | }
42 |
43 | @Override
44 | public void onViewCreated(View view, Bundle savedInstanceState) {
45 | super.onViewCreated(view, savedInstanceState);
46 | if (savedInstanceState != null && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) {
47 | setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION));
48 | }
49 | }
50 |
51 | @Override
52 | public void onAttach(Activity activity) {
53 | super.onAttach(activity);
54 | if (!(activity instanceof Callbacks)) {
55 | throw new IllegalStateException("Activity must implement fragment's callbacks.");
56 | }
57 |
58 | mCallbacks = (Callbacks) activity;
59 | }
60 |
61 | @Override
62 | public void onDetach() {
63 | super.onDetach();
64 | mCallbacks = sDummyCallbacks;
65 | }
66 |
67 | @Override
68 | public void onListItemClick(ListView listView, View view, int position, long id) {
69 | super.onListItemClick(listView, view, position, id);
70 | mCallbacks.onItemSelected(String.valueOf(position));
71 | }
72 |
73 | @Override
74 | public void onSaveInstanceState(Bundle outState) {
75 | super.onSaveInstanceState(outState);
76 | if (mActivatedPosition != ListView.INVALID_POSITION) {
77 | outState.putInt(STATE_ACTIVATED_POSITION, mActivatedPosition);
78 | }
79 | }
80 |
81 | public void setActivateOnItemClick(boolean activateOnItemClick) {
82 | getListView().setChoiceMode(activateOnItemClick
83 | ? ListView.CHOICE_MODE_SINGLE
84 | : ListView.CHOICE_MODE_NONE);
85 | }
86 |
87 | public void setActivatedPosition(int position) {
88 | if (position == ListView.INVALID_POSITION) {
89 | getListView().setItemChecked(mActivatedPosition, false);
90 | } else {
91 | getListView().setItemChecked(position, true);
92 | }
93 |
94 | mActivatedPosition = position;
95 | }
96 |
97 |
98 | @Override
99 | public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
100 | inflater.inflate(R.menu.refreshmenu, menu);
101 | }
102 |
103 | @Override
104 | public boolean onOptionsItemSelected(MenuItem item) {
105 | int id = item.getItemId();
106 | if (id == R.id.actionbar_refresh) {
107 | refreshList();
108 | return true;
109 | }
110 | return false;
111 | }
112 |
113 | private void refreshList(){
114 | rssService = new RssService(this);
115 | rssService.execute(BLOG_URL);
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/com/nerdability/android/adapter/ArticleListAdapter.java:
--------------------------------------------------------------------------------
1 | package com.nerdability.android.adapter;
2 |
3 |
4 | import java.text.ParseException;
5 | import java.text.SimpleDateFormat;
6 | import java.util.Date;
7 | import java.util.List;
8 | import java.util.Locale;
9 |
10 | import android.app.Activity;
11 | import android.graphics.Color;
12 | import android.graphics.Typeface;
13 | import android.util.Log;
14 | import android.view.LayoutInflater;
15 | import android.view.View;
16 | import android.view.ViewGroup;
17 | import android.widget.ArrayAdapter;
18 | import android.widget.LinearLayout;
19 | import android.widget.TextView;
20 |
21 | import com.nerdability.android.R;
22 | import com.nerdability.android.rss.domain.Article;
23 | import com.nerdability.android.util.DateUtils;
24 |
25 |
26 | public class ArticleListAdapter extends ArrayAdapter {
27 |
28 | public ArticleListAdapter(Activity activity, List articles) {
29 | super(activity, 0, articles);
30 | }
31 |
32 |
33 | @Override
34 | public View getView(int position, View convertView, ViewGroup parent) {
35 | Activity activity = (Activity) getContext();
36 | LayoutInflater inflater = activity.getLayoutInflater();
37 |
38 | View rowView = inflater.inflate(R.layout.fragment_article_list, null);
39 | Article article = getItem(position);
40 |
41 |
42 | TextView textView = (TextView) rowView.findViewById(R.id.article_title_text);
43 | textView.setText(article.getTitle());
44 |
45 | TextView dateView = (TextView) rowView.findViewById(R.id.article_listing_smallprint);
46 | String pubDate = article.getPubDate();
47 | SimpleDateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy kk:mm:ss Z", Locale.ENGLISH);
48 | Date pDate;
49 | try {
50 | pDate = df.parse(pubDate);
51 | pubDate = "published " + DateUtils.getDateDifference(pDate) + " by " + article.getAuthor();
52 | } catch (ParseException e) {
53 | Log.e("DATE PARSING", "Error parsing date..");
54 | pubDate = "published by " + article.getAuthor();
55 | }
56 | dateView.setText(pubDate);
57 |
58 |
59 | if (!article.isRead()){
60 | LinearLayout row = (LinearLayout) rowView.findViewById(R.id.article_row_layout);
61 | row.setBackgroundColor(Color.WHITE);
62 | textView.setTypeface(Typeface.DEFAULT_BOLD);
63 | }
64 | return rowView;
65 |
66 | }
67 | }
--------------------------------------------------------------------------------
/src/com/nerdability/android/db/DbAdapter.java:
--------------------------------------------------------------------------------
1 | package com.nerdability.android.db;
2 |
3 | import android.content.ContentValues;
4 | import android.content.Context;
5 | import android.database.Cursor;
6 | import android.database.SQLException;
7 | import android.database.sqlite.SQLiteDatabase;
8 | import android.database.sqlite.SQLiteDatabase.CursorFactory;
9 | import android.database.sqlite.SQLiteOpenHelper;
10 | import android.provider.BaseColumns;
11 |
12 | import com.nerdability.android.rss.domain.Article;
13 |
14 | public class DbAdapter{
15 |
16 | public static final String KEY_ROWID = BaseColumns._ID;
17 | public static final String KEY_GUID = "guid";
18 | public static final String KEY_READ = "read";
19 | public static final String KEY_OFFLINE = "offline";
20 |
21 | private static final String DATABASE_NAME = "blogposts";
22 | private static final String DATABASE_TABLE = "blogpostlist";
23 | private static final int DATABASE_VERSION = 1;
24 |
25 | private static final String DATABASE_CREATE_LIST_TABLE = "create table " + DATABASE_TABLE + " (" +
26 | KEY_ROWID +" integer primary key autoincrement, "+
27 | KEY_GUID + " text not null, " +
28 | KEY_READ + " boolean not null, " +
29 | KEY_OFFLINE + " boolean not null);";
30 |
31 |
32 | private SQLiteHelper sqLiteHelper;
33 | private SQLiteDatabase sqLiteDatabase;
34 | private Context context;
35 |
36 | public DbAdapter(Context c){
37 | context = c;
38 | }
39 |
40 | public DbAdapter openToRead() throws android.database.SQLException {
41 | sqLiteHelper = new SQLiteHelper(context, DATABASE_NAME, null, DATABASE_VERSION);
42 | sqLiteDatabase = sqLiteHelper.getReadableDatabase();
43 | return this;
44 | }
45 |
46 | public DbAdapter openToWrite() throws android.database.SQLException {
47 | sqLiteHelper = new SQLiteHelper(context, DATABASE_NAME, null, DATABASE_VERSION);
48 | sqLiteDatabase = sqLiteHelper.getWritableDatabase();
49 | return this;
50 | }
51 |
52 | public void close(){
53 | sqLiteHelper.close();
54 | }
55 |
56 | public class SQLiteHelper extends SQLiteOpenHelper {
57 | public SQLiteHelper(Context context, String name, CursorFactory factory, int version) {
58 | super(context, name, factory, version);
59 | }
60 | @Override
61 | public void onCreate(SQLiteDatabase db) {
62 | db.execSQL(DATABASE_CREATE_LIST_TABLE);
63 | }
64 | @Override
65 | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
66 | db.execSQL("DROP TABLE IF EXISTS " + DATABASE_TABLE );
67 | onCreate(db);
68 | }
69 | }
70 |
71 | public long insertBlogListing(String guid) {
72 | ContentValues initialValues = new ContentValues();
73 | initialValues.put(KEY_GUID, guid);
74 | initialValues.put(KEY_READ, false);
75 | initialValues.put(KEY_OFFLINE, false);
76 | return sqLiteDatabase.insert(DATABASE_TABLE, null, initialValues);
77 | }
78 |
79 | public Article getBlogListing(String guid) throws SQLException {
80 | Cursor mCursor =
81 | sqLiteDatabase.query(true, DATABASE_TABLE, new String[] {
82 | KEY_ROWID,
83 | KEY_GUID,
84 | KEY_READ,
85 | KEY_OFFLINE
86 | },
87 | KEY_GUID + "= '" + guid + "'",
88 | null,
89 | null,
90 | null,
91 | null,
92 | null);
93 | if (mCursor != null && mCursor.getCount() > 0) {
94 | mCursor.moveToFirst();
95 | Article a = new Article();
96 | a.setGuid(mCursor.getString(mCursor.getColumnIndex(KEY_GUID)));
97 | a.setRead(mCursor.getInt(mCursor.getColumnIndex(KEY_READ)) > 0);
98 | a.setDbId(mCursor.getLong(mCursor.getColumnIndex(KEY_ROWID)));
99 | a.setOffline(mCursor.getInt(mCursor.getColumnIndex(KEY_OFFLINE)) > 0);
100 | return a;
101 | }
102 | return null;
103 | }
104 |
105 | public boolean markAsUnread(String guid) {
106 | ContentValues args = new ContentValues();
107 | args.put(KEY_READ, false);
108 | return sqLiteDatabase.update(DATABASE_TABLE, args, KEY_GUID + "='" + guid+"'", null) > 0;
109 | }
110 |
111 | public boolean markAsRead(String guid) {
112 | ContentValues args = new ContentValues();
113 | args.put(KEY_READ, true);
114 | return sqLiteDatabase.update(DATABASE_TABLE, args, KEY_GUID + "='" + guid+"'", null) > 0;
115 | }
116 |
117 | public boolean saveForOffline(String guid) {
118 | ContentValues args = new ContentValues();
119 | args.put(KEY_OFFLINE, true);
120 | return sqLiteDatabase.update(DATABASE_TABLE, args, KEY_GUID + "='" + guid+"'", null) > 0;
121 | }
122 | }
--------------------------------------------------------------------------------
/src/com/nerdability/android/rss/RssService.java:
--------------------------------------------------------------------------------
1 | package com.nerdability.android.rss;
2 |
3 | import java.io.IOException;
4 | import java.net.URL;
5 | import java.util.List;
6 |
7 | import javax.xml.parsers.ParserConfigurationException;
8 | import javax.xml.parsers.SAXParser;
9 | import javax.xml.parsers.SAXParserFactory;
10 |
11 | import org.xml.sax.InputSource;
12 | import org.xml.sax.SAXException;
13 | import org.xml.sax.XMLReader;
14 |
15 | import android.app.ProgressDialog;
16 | import android.content.Context;
17 | import android.os.AsyncTask;
18 | import android.util.Log;
19 |
20 | import com.nerdability.android.ArticleListFragment;
21 | import com.nerdability.android.adapter.ArticleListAdapter;
22 | import com.nerdability.android.db.DbAdapter;
23 | import com.nerdability.android.rss.domain.Article;
24 | import com.nerdability.android.rss.parser.RssHandler;
25 |
26 |
27 | public class RssService extends AsyncTask> {
28 |
29 | private ProgressDialog progress;
30 | private Context context;
31 | private ArticleListFragment articleListFrag;
32 |
33 | public RssService(ArticleListFragment articleListFragment) {
34 | context = articleListFragment.getActivity();
35 | articleListFrag = articleListFragment;
36 | progress = new ProgressDialog(context);
37 | progress.setMessage("Loading...");
38 | }
39 |
40 |
41 | protected void onPreExecute() {
42 | Log.e("ASYNC", "PRE EXECUTE");
43 | progress.show();
44 | }
45 |
46 |
47 | protected void onPostExecute(final List articles) {
48 | Log.e("ASYNC", "POST EXECUTE");
49 | articleListFrag.getActivity().runOnUiThread(new Runnable() {
50 | @Override
51 | public void run() {
52 | for (Article a : articles){
53 | Log.d("DB", "Searching DB for GUID: " + a.getGuid());
54 | DbAdapter dba = new DbAdapter(articleListFrag.getActivity());
55 | dba.openToRead();
56 | Article fetchedArticle = dba.getBlogListing(a.getGuid());
57 | dba.close();
58 | if (fetchedArticle == null){
59 | Log.d("DB", "Found entry for first time: " + a.getTitle());
60 | dba = new DbAdapter(articleListFrag.getActivity());
61 | dba.openToWrite();
62 | dba.insertBlogListing(a.getGuid());
63 | dba.close();
64 | }else{
65 | a.setDbId(fetchedArticle.getDbId());
66 | a.setOffline(fetchedArticle.isOffline());
67 | a.setRead(fetchedArticle.isRead());
68 | }
69 | }
70 | ArticleListAdapter adapter = new ArticleListAdapter(articleListFrag.getActivity(), articles);
71 | articleListFrag.setListAdapter(adapter);
72 | adapter.notifyDataSetChanged();
73 |
74 | }
75 | });
76 | progress.dismiss();
77 | }
78 |
79 |
80 | @Override
81 | protected List doInBackground(String... urls) {
82 | String feed = urls[0];
83 |
84 | URL url = null;
85 | try {
86 |
87 | SAXParserFactory spf = SAXParserFactory.newInstance();
88 | SAXParser sp = spf.newSAXParser();
89 | XMLReader xr = sp.getXMLReader();
90 |
91 | url = new URL(feed);
92 | RssHandler rh = new RssHandler();
93 |
94 | xr.setContentHandler(rh);
95 | xr.parse(new InputSource(url.openStream()));
96 |
97 |
98 | Log.e("ASYNC", "PARSING FINISHED");
99 | return rh.getArticleList();
100 |
101 | } catch (IOException e) {
102 | Log.e("RSS Handler IO", e.getMessage() + " >> " + e.toString());
103 | } catch (SAXException e) {
104 | Log.e("RSS Handler SAX", e.toString());
105 | e.printStackTrace();
106 | } catch (ParserConfigurationException e) {
107 | Log.e("RSS Handler Parser Config", e.toString());
108 | }
109 |
110 | return null;
111 |
112 | }
113 | }
--------------------------------------------------------------------------------
/src/com/nerdability/android/rss/domain/Article.java:
--------------------------------------------------------------------------------
1 | package com.nerdability.android.rss.domain;
2 |
3 | import java.io.Serializable;
4 | import java.net.URL;
5 |
6 | public class Article implements Serializable {
7 |
8 | public static final String KEY = "ARTICLE";
9 |
10 | private static final long serialVersionUID = 1L;
11 | private String guid;
12 | private String title;
13 | private String description;
14 | private String pubDate;
15 | private String author;
16 | private URL url;
17 | private String encodedContent;
18 | private boolean read;
19 | private boolean offline;
20 | private long dbId;
21 |
22 |
23 | public String getGuid() {
24 | return guid;
25 | }
26 |
27 | public void setGuid(String guid) {
28 | this.guid = guid;
29 | }
30 |
31 | public String getTitle() {
32 | return title;
33 | }
34 |
35 | public void setTitle(String title) {
36 | this.title = title;
37 | }
38 |
39 | public URL getUrl() {
40 | return url;
41 | }
42 |
43 | public void setUrl(URL url) {
44 | this.url = url;
45 | }
46 |
47 | public void setDescription(String description) {
48 | this.description = extractCData(description);
49 | }
50 |
51 | public String getDescription() {
52 | return description;
53 | }
54 |
55 | public void setPubDate(String pubDate) {
56 | this.pubDate = pubDate;
57 | }
58 |
59 | public String getPubDate() {
60 | return pubDate;
61 | }
62 |
63 | public String getAuthor() {
64 | return author;
65 | }
66 |
67 | public void setAuthor(String author) {
68 | this.author = author;
69 | }
70 |
71 | public void setEncodedContent(String encodedContent) {
72 | this.encodedContent = extractCData(encodedContent);
73 | }
74 |
75 | public String getEncodedContent() {
76 | return encodedContent;
77 | }
78 |
79 | public boolean isRead() {
80 | return read;
81 | }
82 |
83 | public void setRead(boolean read) {
84 | this.read = read;
85 | }
86 |
87 | public boolean isOffline() {
88 | return offline;
89 | }
90 |
91 | public void setOffline(boolean offline) {
92 | this.offline = offline;
93 | }
94 |
95 | public long getDbId() {
96 | return dbId;
97 | }
98 |
99 | public void setDbId(long dbId) {
100 | this.dbId = dbId;
101 | }
102 |
103 | private String extractCData(String data){
104 | data = data.replaceAll("", "");
106 | return data;
107 | }
108 |
109 | }
--------------------------------------------------------------------------------
/src/com/nerdability/android/rss/parser/RssHandler.java:
--------------------------------------------------------------------------------
1 | package com.nerdability.android.rss.parser;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | import org.xml.sax.Attributes;
7 | import org.xml.sax.SAXException;
8 | import org.xml.sax.helpers.DefaultHandler;
9 |
10 | import com.nerdability.android.rss.domain.Article;
11 |
12 |
13 | public class RssHandler extends DefaultHandler {
14 |
15 | // Feed and Article objects to use for temporary storage
16 | private Article currentArticle = new Article();
17 | private List articleList = new ArrayList();
18 |
19 | // Number of articles added so far
20 | private int articlesAdded = 0;
21 |
22 | // Number of articles to download
23 | private static final int ARTICLES_LIMIT = 15;
24 |
25 | //Current characters being accumulated
26 | StringBuffer chars = new StringBuffer();
27 |
28 |
29 | public List getArticleList() {
30 | return articleList;
31 | }
32 |
33 |
34 |
35 | /*
36 | * This method is called everytime a start element is found (an opening XML marker)
37 | * here we always reset the characters StringBuffer as we are only currently interested
38 | * in the the text values stored at leaf nodes
39 | *
40 | * (non-Javadoc)
41 | * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
42 | */
43 | public void startElement(String uri, String localName, String qName, Attributes atts) {
44 | chars = new StringBuffer();
45 | }
46 |
47 |
48 |
49 | /*
50 | * This method is called everytime an end element is found (a closing XML marker)
51 | * here we check what element is being closed, if it is a relevant leaf node that we are
52 | * checking, such as Title, then we get the characters we have accumulated in the StringBuffer
53 | * and set the current Article's title to the value
54 | *
55 | * If this is closing the "entry", it means it is the end of the article, so we add that to the list
56 | * and then reset our Article object for the next one on the stream
57 | *
58 | *
59 | * (non-Javadoc)
60 | * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
61 | */
62 | public void endElement(String uri, String localName, String qName) throws SAXException {
63 |
64 | if (localName.equalsIgnoreCase("title")){
65 | currentArticle.setTitle(chars.toString());
66 | } else if (localName.equalsIgnoreCase("description")){
67 | currentArticle.setDescription(chars.toString());
68 | } else if (localName.equalsIgnoreCase("published")){
69 | currentArticle.setPubDate(chars.toString());
70 | } else if (localName.equalsIgnoreCase("id")){
71 | currentArticle.setGuid(chars.toString());
72 | } else if (localName.equalsIgnoreCase("author")){
73 | currentArticle.setAuthor(chars.toString());
74 | } else if (localName.equalsIgnoreCase("content")){
75 | currentArticle.setEncodedContent(chars.toString());
76 | } else if (localName.equalsIgnoreCase("entry")){
77 |
78 | }
79 |
80 |
81 | // Check if looking for article, and if article is complete
82 | if (localName.equalsIgnoreCase("entry")) {
83 |
84 | articleList.add(currentArticle);
85 |
86 | currentArticle = new Article();
87 |
88 | // Lets check if we've hit our limit on number of articles
89 | articlesAdded++;
90 | if (articlesAdded >= ARTICLES_LIMIT)
91 | {
92 | throw new SAXException();
93 | }
94 | }
95 | }
96 |
97 |
98 | /*
99 | * This method is called when characters are found in between XML markers, however, there is no
100 | * guarante that this will be called at the end of the node, or that it will be called only once
101 | * , so we just accumulate these and then deal with them in endElement() to be sure we have all the
102 | * text
103 | *
104 | * (non-Javadoc)
105 | * @see org.xml.sax.helpers.DefaultHandler#characters(char[], int, int)
106 | */
107 | public void characters(char ch[], int start, int length) {
108 | chars.append(new String(ch, start, length));
109 | }
110 | }
--------------------------------------------------------------------------------
/src/com/nerdability/android/util/DateUtils.java:
--------------------------------------------------------------------------------
1 | package com.nerdability.android.util;
2 |
3 | import java.util.Calendar;
4 | import java.util.Date;
5 |
6 | public class DateUtils {
7 |
8 | public static String getDateDifference(Date thenDate){
9 | Calendar now = Calendar.getInstance();
10 | Calendar then = Calendar.getInstance();
11 | now.setTime(new Date());
12 | then.setTime(thenDate);
13 |
14 |
15 | // Get the represented date in milliseconds
16 | long nowMs = now.getTimeInMillis();
17 | long thenMs = then.getTimeInMillis();
18 |
19 | // Calculate difference in milliseconds
20 | long diff = nowMs - thenMs;
21 |
22 | // Calculate difference in seconds
23 | long diffMinutes = diff / (60 * 1000);
24 | long diffHours = diff / (60 * 60 * 1000);
25 | long diffDays = diff / (24 * 60 * 60 * 1000);
26 |
27 | if (diffMinutes<60){
28 | if (diffMinutes==1)
29 | return diffMinutes + " minute ago";
30 | else
31 | return diffMinutes + " minutes ago";
32 | } else if (diffHours<24){
33 | if (diffHours==1)
34 | return diffHours + " hour ago";
35 | else
36 | return diffHours + " hours ago";
37 | }else if (diffDays<30){
38 | if (diffDays==1)
39 | return diffDays + " day ago";
40 | else
41 | return diffDays + " days ago";
42 | }else {
43 | return "a long time ago..";
44 | }
45 | }
46 |
47 |
48 | }
49 |
--------------------------------------------------------------------------------