├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── raw │ │ │ │ ├── ic_heart.png │ │ │ │ ├── ic_download.png │ │ │ │ └── test_feed.json │ │ │ ├── drawable-hdpi │ │ │ │ ├── ic_back.png │ │ │ │ ├── ic_search.png │ │ │ │ ├── ic_share.png │ │ │ │ ├── ic_sort.png │ │ │ │ ├── ic_tech.png │ │ │ │ ├── ic_business.png │ │ │ │ ├── ic_filters.png │ │ │ │ ├── ic_category_3.png │ │ │ │ └── ic_side_menu.png │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── drawable │ │ │ │ ├── list_selector.xml │ │ │ │ ├── bg_search.xml │ │ │ │ ├── ripple.xml │ │ │ │ └── text_selector.xml │ │ │ ├── drawable-v21 │ │ │ │ └── ripple.xml │ │ │ ├── layout │ │ │ │ ├── view_progress.xml │ │ │ │ ├── activity_webview.xml │ │ │ │ ├── view_bottom_sheet.xml │ │ │ │ ├── view_error.xml │ │ │ │ ├── activity_main.xml │ │ │ │ ├── fragment_list_item.xml │ │ │ │ ├── activity_feed.xml │ │ │ │ ├── fragment_main.xml │ │ │ │ ├── bottom_list_item.xml │ │ │ │ ├── view_toolbar.xml │ │ │ │ └── feed_list_item.xml │ │ │ └── values │ │ │ │ ├── styles.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── colors.xml │ │ │ │ └── strings.xml │ │ ├── assets │ │ │ └── fonts │ │ │ │ └── tiempos_h_medium.otf │ │ ├── java │ │ │ └── com │ │ │ │ └── an │ │ │ │ └── inshorts │ │ │ │ ├── service │ │ │ │ ├── ResponseService.java │ │ │ │ ├── ResponseServiceImpl.java │ │ │ │ ├── AbstractServiceImpl.java │ │ │ │ ├── FeedService.java │ │ │ │ ├── DataService.java │ │ │ │ ├── DataServiceImpl.java │ │ │ │ └── FeedServiceImpl.java │ │ │ │ ├── listener │ │ │ │ ├── MenuItemListener.java │ │ │ │ ├── OnViewItemClickListener.java │ │ │ │ └── OnFeedChangeListener.java │ │ │ │ ├── callback │ │ │ │ └── RESTListener.java │ │ │ │ ├── fragment │ │ │ │ ├── BaseFragment.java │ │ │ │ └── MainFragment.java │ │ │ │ ├── model │ │ │ │ ├── MenuItem.java │ │ │ │ └── Feed.java │ │ │ │ ├── db │ │ │ │ ├── DbExecutorService.java │ │ │ │ ├── FeedDbTask.java │ │ │ │ ├── FeedModule.java │ │ │ │ └── FeedDb.java │ │ │ │ ├── rest │ │ │ │ ├── RESTExecutorService.java │ │ │ │ ├── RESTApiTask.java │ │ │ │ ├── RESTService.java │ │ │ │ └── RESTCacheRequest.java │ │ │ │ ├── utils │ │ │ │ ├── CollectionUtils.java │ │ │ │ ├── ConnectivityStatus.java │ │ │ │ ├── NavigatorUtils.java │ │ │ │ ├── ObjectUtil.java │ │ │ │ └── BaseUtils.java │ │ │ │ ├── dialogs │ │ │ │ ├── BottomSheetHelper.java │ │ │ │ └── CustomBottomSheetDialog.java │ │ │ │ ├── BaseApplication.java │ │ │ │ ├── views │ │ │ │ ├── AspectRatioCardView.java │ │ │ │ ├── PaginationScrollListener.java │ │ │ │ ├── CustomPageTransformer.java │ │ │ │ ├── CustomWebViewClient.java │ │ │ │ ├── RecyclerItemClickListener.java │ │ │ │ ├── CustomViewPager.java │ │ │ │ └── MenuCreator.java │ │ │ │ ├── BaseConstants.java │ │ │ │ ├── adapter │ │ │ │ ├── MainPagerAdapter.java │ │ │ │ ├── MenuItemAdapter.java │ │ │ │ ├── FeedListAdapter.java │ │ │ │ └── NewsListAdapter.java │ │ │ │ └── activity │ │ │ │ ├── CustomWebViewActivity.java │ │ │ │ ├── BaseActivity.java │ │ │ │ ├── MainActivity.java │ │ │ │ └── FeedListActivity.java │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── an │ │ │ └── inshorts │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── com │ │ └── an │ │ └── inshorts │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── README.md ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── gradle.properties ├── gradlew.bat └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inshorts 2 | A demo app news app for a hackathon - includes MVP architecture example 3 | -------------------------------------------------------------------------------- /app/src/main/res/raw/ic_heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitaa1990/Inshorts/HEAD/app/src/main/res/raw/ic_heart.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitaa1990/Inshorts/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/raw/ic_download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitaa1990/Inshorts/HEAD/app/src/main/res/raw/ic_download.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitaa1990/Inshorts/HEAD/app/src/main/res/drawable-hdpi/ic_back.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitaa1990/Inshorts/HEAD/app/src/main/res/drawable-hdpi/ic_search.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitaa1990/Inshorts/HEAD/app/src/main/res/drawable-hdpi/ic_share.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_sort.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitaa1990/Inshorts/HEAD/app/src/main/res/drawable-hdpi/ic_sort.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_tech.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitaa1990/Inshorts/HEAD/app/src/main/res/drawable-hdpi/ic_tech.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitaa1990/Inshorts/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitaa1990/Inshorts/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/assets/fonts/tiempos_h_medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitaa1990/Inshorts/HEAD/app/src/main/assets/fonts/tiempos_h_medium.otf -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_business.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitaa1990/Inshorts/HEAD/app/src/main/res/drawable-hdpi/ic_business.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_filters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitaa1990/Inshorts/HEAD/app/src/main/res/drawable-hdpi/ic_filters.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitaa1990/Inshorts/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitaa1990/Inshorts/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_category_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitaa1990/Inshorts/HEAD/app/src/main/res/drawable-hdpi/ic_category_3.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_side_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitaa1990/Inshorts/HEAD/app/src/main/res/drawable-hdpi/ic_side_menu.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitaa1990/Inshorts/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/service/ResponseService.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.service; 2 | 3 | public interface ResponseService { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/listener/MenuItemListener.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.listener; 2 | 3 | import com.an.inshorts.model.MenuItem; 4 | 5 | public interface MenuItemListener { 6 | void onMenuItemClick(MenuItem item); 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/callback/RESTListener.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.callback; 2 | 3 | 4 | import com.android.volley.VolleyError; 5 | 6 | public interface RESTListener { 7 | 8 | void onSuccess(Object response); 9 | void onError(VolleyError error); 10 | } 11 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 28 10:00:20 PST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/listener/OnViewItemClickListener.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.listener; 2 | 3 | 4 | public interface OnViewItemClickListener { 5 | void onFavClick(int position, boolean checked); 6 | void onOfflineClick(int position, boolean checked); 7 | void onViewClick(int position); 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/list_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/listener/OnFeedChangeListener.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.listener; 2 | 3 | 4 | import com.an.inshorts.model.Feed; 5 | 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | public interface OnFeedChangeListener { 10 | 11 | void showError(String message); 12 | void refreshFeed(Map> data); 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_search.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/service/ResponseServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.service; 2 | 3 | import android.content.Context; 4 | 5 | 6 | /* 7 | * Class to handle the response returned from api 8 | * 9 | * */ 10 | 11 | public class ResponseServiceImpl extends DataServiceImpl implements ResponseService { 12 | 13 | 14 | public ResponseServiceImpl(Context context) { 15 | super(context); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ripple.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ripple.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/test/java/com/an/inshorts/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/service/AbstractServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.service; 2 | 3 | import android.content.Context; 4 | 5 | import com.an.inshorts.BaseConstants; 6 | import com.an.inshorts.db.FeedModule; 7 | 8 | public abstract class AbstractServiceImpl implements BaseConstants { 9 | 10 | protected Context context; 11 | protected FeedModule feedModule; 12 | public AbstractServiceImpl(Context context) { 13 | this.context = context; 14 | this.feedModule = FeedModule.getInstance(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/fragment/BaseFragment.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.fragment; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.support.v4.app.Fragment; 6 | 7 | import com.an.inshorts.BaseConstants; 8 | 9 | public class BaseFragment extends Fragment implements BaseConstants { 10 | 11 | protected Activity activity; 12 | 13 | @Override 14 | public void onAttach(Context context) { 15 | super.onAttach(context); 16 | this.activity = (Activity) context; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/model/MenuItem.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.model; 2 | 3 | import java.io.Serializable; 4 | 5 | public class MenuItem implements Serializable { 6 | 7 | private int mDrawableRes; 8 | private String mTitle; 9 | 10 | public MenuItem(int drawable, String title) { 11 | mDrawableRes = drawable; 12 | mTitle = title; 13 | } 14 | 15 | public int getDrawableResource() { 16 | return mDrawableRes; 17 | } 18 | 19 | public String getTitle() { 20 | return mTitle; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/db/DbExecutorService.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.db; 2 | 3 | import java.util.concurrent.ExecutorService; 4 | import java.util.concurrent.Executors; 5 | 6 | public class DbExecutorService { 7 | 8 | private static final ExecutorService _appDbThreadPool = Executors.newFixedThreadPool(5); 9 | 10 | public static void submit(Runnable runnable) { 11 | getAppDbThreadPool().submit(runnable); 12 | } 13 | 14 | private static ExecutorService getAppDbThreadPool() { 15 | return _appDbThreadPool; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/rest/RESTExecutorService.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.rest; 2 | 3 | 4 | import java.util.concurrent.ExecutorService; 5 | import java.util.concurrent.Executors; 6 | 7 | public class RESTExecutorService { 8 | 9 | private static final ExecutorService _restAPIThreadPool = Executors.newFixedThreadPool(5); 10 | 11 | public static void submit(Runnable runnable) { 12 | getRestAPIThreadPool().submit(runnable); 13 | } 14 | 15 | private static ExecutorService getRestAPIThreadPool() { 16 | return _restAPIThreadPool; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/view_progress.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/text_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/anitaa/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/service/FeedService.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.service; 2 | 3 | 4 | import com.an.inshorts.model.Feed; 5 | 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | public interface FeedService { 10 | 11 | void handleAction(String type, Feed feed, boolean checked); 12 | Map> handleMenuItemClick(String method, List data); 13 | boolean isFavourite(Long id); 14 | boolean isOfflineFeed(Long id); 15 | List sortFeed(String type, List data); 16 | Map> filterFeed(String type, List data); 17 | Map> fetchFavouriteFeeds(); 18 | Map> fetchOfflineFeeds(); 19 | List loadMoreFeed(String type, String name, int limit); 20 | } 21 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/utils/CollectionUtils.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.utils; 2 | 3 | import com.an.inshorts.model.Feed; 4 | import java.util.Collections; 5 | import java.util.Comparator; 6 | import java.util.List; 7 | 8 | public class CollectionUtils { 9 | 10 | public static List sortFeedAsc(List data) { 11 | Collections.sort(data, new Comparator() { 12 | public int compare(Feed o1, Feed o2) { 13 | return o1.getTimestamp().compareTo(o2.getTimestamp()); 14 | } 15 | }); 16 | return data; 17 | } 18 | 19 | public static List sortFeedDesc(List data) { 20 | List sortedData = sortFeedAsc(data); 21 | Collections.reverse(sortedData); 22 | return sortedData; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_webview.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/utils/ConnectivityStatus.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.utils; 2 | 3 | import android.content.Context; 4 | import android.content.ContextWrapper; 5 | import android.net.ConnectivityManager; 6 | import android.net.NetworkInfo; 7 | 8 | 9 | public class ConnectivityStatus extends ContextWrapper { 10 | 11 | public ConnectivityStatus(Context base) { 12 | super(base); 13 | } 14 | 15 | public static boolean isConnected(Context context){ 16 | 17 | ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 18 | NetworkInfo connection = manager.getActiveNetworkInfo(); 19 | if (connection != null && connection.isConnectedOrConnecting()){ 20 | return true; 21 | } 22 | return false; 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/dialogs/BottomSheetHelper.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.dialogs; 2 | 3 | import android.content.Context; 4 | 5 | import com.an.inshorts.listener.MenuItemListener; 6 | import com.an.inshorts.model.MenuItem; 7 | import java.util.List; 8 | 9 | 10 | public class BottomSheetHelper { 11 | 12 | private static BottomSheetHelper instance; 13 | public static BottomSheetHelper getInstance() { 14 | if(instance == null) instance = new BottomSheetHelper(); 15 | return instance; 16 | } 17 | 18 | private CustomBottomSheetDialog bottomSheetDialog; 19 | 20 | public void showBottomSheet(Context context, List list, MenuItemListener listener) { 21 | bottomSheetDialog = new CustomBottomSheetDialog(context, list, listener); 22 | bottomSheetDialog.show(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/rest/RESTApiTask.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.rest; 2 | 3 | 4 | import android.content.Context; 5 | 6 | import com.an.inshorts.BaseConstants; 7 | import com.an.inshorts.callback.RESTListener; 8 | 9 | public class RESTAPITask implements Runnable, BaseConstants { 10 | 11 | private Context context; 12 | private String method; 13 | private RESTListener restListener; 14 | 15 | public RESTAPITask(Context context, String method, RESTListener restListener) { 16 | this.context = context; 17 | this.method = method; 18 | this.restListener = restListener; 19 | } 20 | 21 | @Override 22 | public void run() { 23 | if (METHOD_NEWS_FEED.equalsIgnoreCase(method)) { 24 | RESTService.getInstance().getNewsFeed(context, restListener); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/view_bottom_sheet.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/service/DataService.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.service; 2 | 3 | import com.an.inshorts.model.Feed; 4 | 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | public interface DataService { 9 | 10 | Map> filterByCategory(List data); 11 | Map> filterByPublisher(List data); 12 | List getFeedsByCategory(String name, int limit); 13 | List getFeedsByPublisher(String name, int limit); 14 | List sortFeedAsc(List data); 15 | List sortFeedDesc(List data); 16 | List getFavouriteFeeds(); 17 | List getOfflineFeeds(); 18 | List getFeeds(); 19 | void addFeedToDb(List feeds); 20 | void addToFavourites(Feed feed); 21 | void removeFromFavourites(Feed feed); 22 | void addToOfflineFeed(Feed feed); 23 | void removeFromOfflineFeed(Feed feed); 24 | boolean isAddedToFavourite(Long id); 25 | boolean isAvailableOffline(Long id); 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/utils/NavigatorUtils.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.utils; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | 6 | import com.an.inshorts.BaseConstants; 7 | import com.an.inshorts.activity.CustomWebViewActivity; 8 | import com.an.inshorts.activity.FeedListActivity; 9 | import com.an.inshorts.model.Feed; 10 | 11 | import java.io.Serializable; 12 | import java.util.List; 13 | 14 | public class NavigatorUtils implements BaseConstants { 15 | 16 | public static void openWebView(Context context, Feed feed) { 17 | Intent intent = new Intent(context, CustomWebViewActivity.class); 18 | intent.putExtra(INTENT_URL, feed.getUrl()); 19 | intent.putExtra(INTENT_CATEGORY_NAME, CATEGORY.get(feed.getCategory())); 20 | context.startActivity(intent); 21 | } 22 | 23 | public static void openFeedScreen(Context context, String name, List data) { 24 | Intent intent = new Intent(context, FeedListActivity.class); 25 | intent.putExtra(INTENT_FEED, (Serializable) data); 26 | intent.putExtra(INTENT_CATEGORY_NAME, name); 27 | context.startActivity(intent); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 18 | 19 | 20 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/BaseApplication.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import com.android.volley.Request; 7 | import com.android.volley.RequestQueue; 8 | import com.android.volley.toolbox.Volley; 9 | 10 | public class BaseApplication extends Application { 11 | 12 | private static BaseApplication mInstance; 13 | private RequestQueue mRequestQueue; 14 | 15 | private static Context mCtx; 16 | 17 | private BaseApplication(Context context) { 18 | mCtx = context; 19 | mRequestQueue = getRequestQueue(); 20 | } 21 | 22 | public static synchronized BaseApplication getInstance(Context context) { 23 | if (mInstance == null) { 24 | mInstance = new BaseApplication(context); 25 | } 26 | return mInstance; 27 | } 28 | 29 | public RequestQueue getRequestQueue() { 30 | if (mRequestQueue == null) { 31 | mRequestQueue = Volley.newRequestQueue(mCtx.getApplicationContext()); 32 | } 33 | return mRequestQueue; 34 | } 35 | 36 | public void addToRequestQueue(Request req) { 37 | getRequestQueue().add(req); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/views/AspectRatioCardView.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.views; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.CardView; 5 | import android.util.AttributeSet; 6 | import android.view.ViewGroup; 7 | 8 | public class AspectRatioCardView extends CardView { 9 | 10 | private float ratio = 1.4f; 11 | 12 | public AspectRatioCardView(Context context) { 13 | this(context, null); 14 | } 15 | 16 | public AspectRatioCardView(Context context, AttributeSet attrs) { 17 | this(context, attrs, 0); 18 | } 19 | 20 | public AspectRatioCardView(Context context, AttributeSet attrs, int defStyleAttr) { 21 | super(context, attrs, defStyleAttr); 22 | } 23 | 24 | @Override 25 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 26 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 27 | if (ratio > 0) { 28 | int ratioHeight = (int) (getMeasuredWidth() * ratio); 29 | setMeasuredDimension(getMeasuredWidth(), ratioHeight); 30 | ViewGroup.LayoutParams lp = getLayoutParams(); 31 | lp.height = ratioHeight; 32 | setLayoutParams(lp); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/views/PaginationScrollListener.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.views; 2 | 3 | 4 | import android.support.v7.widget.LinearLayoutManager; 5 | import android.support.v7.widget.RecyclerView; 6 | 7 | public abstract class PaginationScrollListener extends RecyclerView.OnScrollListener { 8 | 9 | private LinearLayoutManager layoutManager; 10 | 11 | public PaginationScrollListener(LinearLayoutManager layoutManager) { 12 | this.layoutManager = layoutManager; 13 | } 14 | 15 | @Override 16 | public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 17 | super.onScrolled(recyclerView, dx, dy); 18 | 19 | int visibleItemCount = layoutManager.getChildCount(); 20 | int totalItemCount = layoutManager.getItemCount(); 21 | int firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition(); 22 | 23 | if (!isLoading() && !isLastPage()) { 24 | if ((visibleItemCount + firstVisibleItemPosition) >= totalItemCount 25 | && firstVisibleItemPosition >= 0) { 26 | loadMoreItems(); 27 | } 28 | } 29 | } 30 | 31 | protected abstract void loadMoreItems(); 32 | 33 | public abstract int getTotalPageCount(); 34 | 35 | public abstract boolean isLastPage(); 36 | 37 | public abstract boolean isLoading(); 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/views/CustomPageTransformer.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.views; 2 | 3 | /* 4 | * Page Transformer class to add animation in between page transitions 5 | * 6 | * */ 7 | 8 | import android.support.v4.view.ViewPager; 9 | import android.view.View; 10 | 11 | import com.an.inshorts.R; 12 | 13 | public class CustomPageTransformer implements ViewPager.PageTransformer { 14 | 15 | 16 | 17 | @Override 18 | public void transformPage(View page, float position) { 19 | 20 | 21 | int pageWidth = (int) page.getWidth(); 22 | final float pageWidthTimesPosition = pageWidth * position; 23 | final float absPosition = Math.abs(position); 24 | 25 | View text = page.findViewById(R.id.category_name); 26 | View image = page.findViewById(R.id.category_img); 27 | 28 | if (position <= -1.0f || position >= 1.0f) { 29 | 30 | // Page is not visible -- stop any running animations 31 | 32 | } else if (position == 0.0f) { 33 | 34 | // Page is selected -- reset any views if necessary 35 | 36 | } else { 37 | 38 | if(text != null) { 39 | text.setTranslationX(pageWidthTimesPosition * 0.5f); 40 | } 41 | if(image != null) { 42 | image.setTranslationX(pageWidthTimesPosition * 0.5f); 43 | } 44 | } 45 | 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/BaseConstants.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | public interface BaseConstants { 9 | 10 | String BASE_URL = "http://starlord.hackerearth.com"; 11 | String PATH_NEWS_FEED = "/newsjson"; 12 | 13 | String METHOD_NEWS_FEED = "news_feed"; 14 | String INTENT_URL = "webview_url"; 15 | String INTENT_CATEGORY_NAME = "categoryName"; 16 | String INTENT_FEED = "intent_feed"; 17 | String INTENT_CATEGORIES = "categories"; 18 | 19 | String FILTER_BY_CATEGORY = "category"; 20 | String FILTER_BY_PUBLISHER = "publisher"; 21 | 22 | String ACTION_TYPE_FAV = "favourite"; 23 | String ACTION_TYPE_OFFLINE = "offline"; 24 | String ACTION_TYPE_URL = "open_url"; 25 | String ACTION_TYPE_GET_FEED = "get_feed"; 26 | 27 | String TYPE_ADD_OFFLINE = "add_offline"; 28 | String TYPE_REMOVE_OFFLINE = "remove_offline"; 29 | String TYPE_ADD_FAVOURITES = "add_favourites"; 30 | String TYPE_REMOVE_FAVOURITES = "remove_favourites"; 31 | String TYPE_ADD_FEED = "add_feed"; 32 | 33 | int PAGE_SIZE = 20; 34 | 35 | String LOCALE_CACHE_PATH = "/data/data/com.an.inshorts/inshorts.dat"; 36 | 37 | Map CATEGORY = new HashMap() 38 | {{ 39 | put("b", "Business"); 40 | put("t", "Tech"); 41 | put("e", "Movies"); 42 | put("m", "Health"); 43 | }}; 44 | } 45 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 26 5 | buildToolsVersion "26.0.1" 6 | defaultConfig { 7 | applicationId "com.an.inshorts" 8 | minSdkVersion 19 9 | targetSdkVersion 26 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | compile 'com.android.support:appcompat-v7:26.0.0-alpha1' 28 | compile 'com.android.support:cardview-v7:26.0.0-alpha1' 29 | compile 'com.android.support:recyclerview-v7:26.0.0-alpha1' 30 | compile 'com.android.support:design:26.0.0-alpha1' 31 | testCompile 'junit:junit:4.12' 32 | 33 | /* 34 | * My own library for implementation of custom fonts 35 | * */ 36 | compile 'com.an.customfontview:customfont:0.1.0' 37 | 38 | compile 'com.google.code.gson:gson:2.5' 39 | compile 'com.android.volley:volley:1.0.0' 40 | 41 | 42 | compile 'com.sackcentury:shinebutton:0.1.9' 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/db/FeedDbTask.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.db; 2 | 3 | import android.content.Context; 4 | 5 | import com.an.inshorts.BaseConstants; 6 | import com.an.inshorts.model.Feed; 7 | 8 | import java.util.List; 9 | 10 | public class FeedDbTask implements Runnable, BaseConstants { 11 | 12 | private Context context; 13 | private String method; 14 | private Feed feed; 15 | private List feeds; 16 | public FeedDbTask(Context context, String method, Feed feed) { 17 | this.context = context; 18 | this.method = method; 19 | this.feed = feed; 20 | } 21 | 22 | public FeedDbTask(Context context, String method, List feeds) { 23 | this.context = context; 24 | this.method = method; 25 | this.feeds = feeds; 26 | } 27 | 28 | @Override 29 | public void run() { 30 | if(TYPE_ADD_OFFLINE.equalsIgnoreCase(method)) { 31 | FeedModule.getInstance().addToOfflineFeeds(feed); 32 | 33 | } else if(TYPE_REMOVE_OFFLINE.equalsIgnoreCase(method)) { 34 | FeedModule.getInstance().removeFromOfflineFeeds(feed); 35 | 36 | } else if(TYPE_ADD_FAVOURITES.equalsIgnoreCase(method)) { 37 | FeedModule.getInstance().addToFavourites(feed); 38 | 39 | } else if(TYPE_REMOVE_FAVOURITES.equalsIgnoreCase(method)) { 40 | FeedModule.getInstance().removeFromFavourites(feed); 41 | 42 | } else if(TYPE_ADD_FEED.equalsIgnoreCase(method)) { 43 | FeedModule.getInstance().addFeeds(feeds); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/views/CustomWebViewClient.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.views; 2 | 3 | 4 | import android.app.Activity; 5 | import android.content.Context; 6 | import android.graphics.Bitmap; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewStub; 10 | import android.webkit.WebResourceError; 11 | import android.webkit.WebResourceRequest; 12 | import android.webkit.WebView; 13 | import android.webkit.WebViewClient; 14 | 15 | import com.an.inshorts.R; 16 | 17 | public class CustomWebViewClient extends WebViewClient { 18 | 19 | private WebView webView; 20 | private Activity activity; 21 | public CustomWebViewClient(WebView webView, Activity activity) { 22 | this.webView = webView; 23 | this.activity = activity; 24 | } 25 | 26 | @Override 27 | public void onLoadResource(WebView view, String url) { 28 | 29 | } 30 | 31 | @Override 32 | public void onPageStarted(WebView view, String url, Bitmap favicon) { 33 | super.onPageStarted(view, url, favicon); 34 | } 35 | 36 | @Override 37 | public void onPageFinished(WebView view, String url) { 38 | super.onPageFinished(view, url); 39 | } 40 | 41 | @Override 42 | public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { 43 | super.onReceivedError(view, request, error); 44 | webView.setVisibility(View.GONE); 45 | ViewStub stub = activity.findViewById(R.id.stub_import); 46 | if(stub != null) stub.inflate(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/adapter/MainPagerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.adapter; 2 | 3 | import android.support.v4.app.Fragment; 4 | import android.support.v4.app.FragmentManager; 5 | import android.support.v4.app.FragmentStatePagerAdapter; 6 | 7 | import com.an.inshorts.fragment.MainFragment; 8 | 9 | import java.util.List; 10 | 11 | public class MainPagerAdapter extends FragmentStatePagerAdapter { 12 | 13 | List fragments; 14 | 15 | public MainPagerAdapter(FragmentManager fm) { 16 | super(fm); 17 | } 18 | 19 | public MainPagerAdapter(FragmentManager fm, 20 | List fragments) { 21 | super(fm); 22 | this.fragments = fragments; 23 | } 24 | 25 | @Override 26 | public Fragment getItem(int position) { 27 | MainFragment fragment = fragments.get(position); 28 | return fragment; 29 | } 30 | 31 | @Override 32 | public int getItemPosition(Object object) { 33 | int position = fragments.indexOf(object); 34 | if (position >= 0) { 35 | return position; 36 | } else return POSITION_NONE; 37 | } 38 | 39 | public void addFragment(MainFragment fragment) { 40 | fragments.add(fragment); 41 | } 42 | 43 | public void removeFragments() { 44 | fragments.clear(); 45 | refreshAdapter(); 46 | } 47 | 48 | public void refreshAdapter() { 49 | notifyDataSetChanged(); 50 | } 51 | 52 | @Override 53 | public int getCount() { 54 | return fragments.size(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/views/RecyclerItemClickListener.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.views; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.GestureDetector; 6 | import android.view.MotionEvent; 7 | import android.view.View; 8 | 9 | public class RecyclerItemClickListener implements RecyclerView.OnItemTouchListener { 10 | private OnItemClickListener mListener; 11 | 12 | public interface OnItemClickListener { 13 | public void onItemClick(View view, int position); 14 | } 15 | 16 | GestureDetector mGestureDetector; 17 | 18 | public RecyclerItemClickListener(Context context, OnItemClickListener listener) { 19 | mListener = listener; 20 | mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { 21 | @Override 22 | public boolean onSingleTapUp(MotionEvent e) { 23 | return true; 24 | } 25 | }); 26 | } 27 | 28 | @Override 29 | public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e) { 30 | View childView = view.findChildViewUnder(e.getX(), e.getY()); 31 | if (childView != null && mListener != null && mGestureDetector.onTouchEvent(e)) { 32 | mListener.onItemClick(childView, view.getChildPosition(childView)); 33 | } 34 | return false; 35 | } 36 | 37 | @Override 38 | public void onTouchEvent(RecyclerView view, MotionEvent motionEvent) { 39 | } 40 | 41 | @Override 42 | public void onRequestDisallowInterceptTouchEvent(boolean b) { 43 | 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/view_error.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 16 | 17 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 13 | 14 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 34 | 35 | 36 | 37 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/utils/ObjectUtil.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.utils; 2 | 3 | 4 | import java.io.EOFException; 5 | import java.io.File; 6 | import java.io.FileInputStream; 7 | import java.io.FileOutputStream; 8 | import java.io.IOException; 9 | import java.io.ObjectInputStream; 10 | import java.io.ObjectOutputStream; 11 | 12 | public class ObjectUtil { 13 | 14 | public void writeObjects(T object, String filename) { 15 | ObjectOutputStream out = null; 16 | 17 | try { 18 | out = new ObjectOutputStream(new FileOutputStream(filename)); 19 | out.writeObject(object); 20 | 21 | } catch (Exception e) { 22 | e.printStackTrace(); 23 | } finally { 24 | if (out != null) { 25 | 26 | try { 27 | out.close(); 28 | } catch (IOException e) { 29 | e.printStackTrace(); 30 | } 31 | } 32 | } 33 | } 34 | 35 | @SuppressWarnings("unchecked") 36 | public T readObjects(String fileName) { 37 | File f = new File(fileName); 38 | 39 | if (f.exists()) { 40 | ObjectInputStream in = null; 41 | 42 | try { 43 | in = new ObjectInputStream(new FileInputStream(fileName)); 44 | return (T) in.readObject(); 45 | } catch (Exception e) { 46 | return null; 47 | 48 | } finally { 49 | if (in != null) { 50 | try { 51 | in.close(); 52 | } catch (IOException e) { 53 | } 54 | } 55 | } 56 | } else { 57 | return null; 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 20 | 21 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/rest/RESTService.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.rest; 2 | 3 | import android.content.Context; 4 | 5 | import com.an.inshorts.BaseApplication; 6 | import com.an.inshorts.callback.RESTListener; 7 | import com.android.volley.Request; 8 | import com.android.volley.Response; 9 | import com.android.volley.VolleyError; 10 | 11 | import static com.an.inshorts.BaseConstants.BASE_URL; 12 | import static com.an.inshorts.BaseConstants.PATH_NEWS_FEED; 13 | 14 | public class RESTService { 15 | 16 | private static volatile RESTService instance; 17 | 18 | public static RESTService getInstance() { 19 | if (instance == null) { 20 | synchronized (RESTService.class) { 21 | if (instance == null) instance = new RESTService(); 22 | } 23 | } 24 | return instance; 25 | } 26 | 27 | public void getNewsFeed(Context context, final RESTListener restListener) { 28 | String url = String.format("%s%s", BASE_URL, PATH_NEWS_FEED); 29 | 30 | RESTCacheRequest stringRequest = new RESTCacheRequest(Request.Method.GET, 31 | url, 32 | new Response.Listener() { 33 | @Override 34 | public void onResponse(String response) { 35 | restListener.onSuccess(response); 36 | } 37 | }, 38 | new Response.ErrorListener() { 39 | @Override 40 | public void onErrorResponse(VolleyError error) { 41 | restListener.onError(error); 42 | } 43 | }); 44 | 45 | BaseApplication.getInstance(context).addToRequestQueue(stringRequest); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/db/FeedModule.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.db; 2 | 3 | 4 | import com.an.inshorts.model.Feed; 5 | 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | 10 | public class FeedModule { 11 | 12 | private static volatile FeedModule instance; 13 | 14 | public static FeedModule getInstance() { 15 | if (instance == null) { 16 | synchronized (FeedModule.class) { 17 | if (instance == null) instance = new FeedModule(); 18 | } 19 | } 20 | return instance; 21 | } 22 | 23 | private FeedDb feedDb; 24 | private FeedModule() { 25 | feedDb = FeedDb.getInstance(); 26 | } 27 | 28 | public void addToFavourites(Feed feed) { 29 | feedDb.addToFavFeeds(feed); 30 | } 31 | 32 | public void removeFromFavourites(Feed feed) { 33 | feedDb.removeFromFavFeeds(feed); 34 | } 35 | 36 | public boolean isFavourite(Long id) { 37 | return feedDb.isFavourite(id); 38 | } 39 | 40 | public Map getFavourites() { 41 | return feedDb.getFavFeeds(); 42 | } 43 | 44 | public void addToOfflineFeeds(Feed feed) { 45 | feedDb.addToOfflineFeeds(feed); 46 | } 47 | 48 | public void removeFromOfflineFeeds(Feed feed) { 49 | feedDb.removeFromOfflineFeeds(feed); 50 | } 51 | 52 | public boolean isOffline(long id) { 53 | return feedDb.isOffline(id); 54 | } 55 | 56 | public Map getOfflineFeeds() { 57 | return feedDb.getOfflineFeeds(); 58 | } 59 | 60 | public void addFeeds(List feeds) { 61 | feedDb.setFeedList(feeds); 62 | } 63 | 64 | public List getFeedsFromDb() { 65 | return feedDb.getFeedList(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/model/Feed.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.model; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import java.io.Serializable; 5 | 6 | public class Feed implements Serializable { 7 | 8 | @SerializedName("ID") 9 | private Long id; 10 | 11 | @SerializedName("TITLE") 12 | private String title; 13 | 14 | @SerializedName("URL") 15 | private String url; 16 | 17 | @SerializedName("PUBLISHER") 18 | private String publisher; 19 | 20 | @SerializedName("CATEGORY") 21 | private String category; 22 | 23 | @SerializedName("HOSTNAME") 24 | private String hostname; 25 | 26 | @SerializedName("TIMESTAMP") 27 | private Long timestamp; 28 | 29 | public Long getId() { 30 | return id; 31 | } 32 | 33 | public void setId(Long id) { 34 | this.id = id; 35 | } 36 | 37 | public String getTitle() { 38 | return title; 39 | } 40 | 41 | public void setTitle(String title) { 42 | this.title = title; 43 | } 44 | 45 | public String getUrl() { 46 | return url; 47 | } 48 | 49 | public void setUrl(String url) { 50 | this.url = url; 51 | } 52 | 53 | public String getPublisher() { 54 | return publisher; 55 | } 56 | 57 | public void setPublisher(String publisher) { 58 | this.publisher = publisher; 59 | } 60 | 61 | public String getCategory() { 62 | return category; 63 | } 64 | 65 | public void setCategory(String category) { 66 | this.category = category; 67 | } 68 | 69 | public String getHostname() { 70 | return hostname; 71 | } 72 | 73 | public void setHostname(String hostname) { 74 | this.hostname = hostname; 75 | } 76 | 77 | public Long getTimestamp() { 78 | return timestamp; 79 | } 80 | 81 | public void setTimestamp(Long timestamp) { 82 | this.timestamp = timestamp; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 25 | 26 | 27 | 37 | 38 | 39 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4dip 5 | 5dip 6 | 8dip 7 | 8 | 10dip 9 | 11dip 10 | 11 | 12 | 12dip 13 | 14dip 14 | 15dip 15 | 16dip 16 | 18dip 17 | 22dip 18 | 25dip 19 | 26dip 20 | 28dip 21 | 22 | 23 | 24 | 3dip 25 | 5dip 26 | 10dip 27 | 15dip 28 | 13dip 29 | 20dip 30 | 22dip 31 | 24dip 32 | 29dip 33 | 35dip 34 | 40dip 35 | 60dp 36 | 37 | 4sp 38 | 6sp 39 | 8sp 40 | 10sp 41 | 11sp 42 | 12sp 43 | 14sp 44 | 16sp 45 | 17sp 46 | 18sp 47 | 20sp 48 | 22sp 49 | 25sp 50 | 35sp 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/dialogs/CustomBottomSheetDialog.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.dialogs; 2 | 3 | import android.content.Context; 4 | import android.support.design.widget.BottomSheetDialog; 5 | import android.support.v7.widget.LinearLayoutManager; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.view.View; 8 | 9 | import com.an.inshorts.R; 10 | import com.an.inshorts.adapter.MenuItemAdapter; 11 | import com.an.inshorts.listener.MenuItemListener; 12 | import com.an.inshorts.model.MenuItem; 13 | import com.an.inshorts.views.RecyclerItemClickListener; 14 | 15 | import java.util.List; 16 | 17 | public class CustomBottomSheetDialog extends BottomSheetDialog implements RecyclerItemClickListener.OnItemClickListener { 18 | 19 | private Context context; 20 | private RecyclerView recyclerView; 21 | private List menuItemList; 22 | 23 | private MenuItemAdapter adapter; 24 | private MenuItemListener listener; 25 | 26 | public CustomBottomSheetDialog(Context context, List menuItemList, MenuItemListener listener) { 27 | super(context); 28 | this.context = context; 29 | this.listener = listener; 30 | this.menuItemList = menuItemList; 31 | setDialogView(); 32 | } 33 | 34 | public CustomBottomSheetDialog(Context context) { 35 | super(context); 36 | this.context = context; 37 | setDialogView(); 38 | } 39 | 40 | private void setDialogView() { 41 | View bottomSheetView = getLayoutInflater().inflate(R.layout.view_bottom_sheet, null); 42 | setContentView(bottomSheetView); 43 | 44 | recyclerView = bottomSheetView.findViewById(R.id.bottom_list); 45 | recyclerView.setLayoutManager(new LinearLayoutManager(context)); 46 | adapter = new MenuItemAdapter(menuItemList); 47 | recyclerView.setAdapter(adapter); 48 | recyclerView.addOnItemTouchListener(new RecyclerItemClickListener(context, this)); 49 | } 50 | 51 | @Override 52 | public void onItemClick(View childView, int position) { 53 | dismiss(); 54 | listener.onMenuItemClick(adapter.getItem(position)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_feed.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 37 | 38 | 39 | 40 | 41 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/adapter/MenuItemAdapter.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.adapter; 2 | 3 | 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.ImageView; 9 | import android.widget.TextView; 10 | 11 | import com.an.inshorts.R; 12 | import com.an.inshorts.model.MenuItem; 13 | 14 | import java.util.List; 15 | 16 | public class MenuItemAdapter extends RecyclerView.Adapter { 17 | 18 | private List mItems; 19 | 20 | public MenuItemAdapter(List items) { 21 | mItems = items; 22 | } 23 | 24 | @Override 25 | public CustomViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 26 | return new CustomViewHolder(LayoutInflater.from(parent.getContext()) 27 | .inflate(R.layout.bottom_list_item, parent, false)); 28 | } 29 | 30 | @Override 31 | public void onBindViewHolder(CustomViewHolder holder, int position) { 32 | MenuItem menuItem = mItems.get(position); 33 | holder.setData(menuItem); 34 | } 35 | 36 | @Override 37 | public int getItemCount() { 38 | return mItems.size(); 39 | } 40 | 41 | public MenuItem getItem(int position) { 42 | return mItems.get(position); 43 | } 44 | 45 | public class CustomViewHolder extends RecyclerView.ViewHolder { 46 | 47 | private ImageView imageView; 48 | private TextView textView; 49 | 50 | private MenuItem menuItem; 51 | public CustomViewHolder(View itemView) { 52 | super(itemView); 53 | imageView = itemView.findViewById(R.id.menu_img); 54 | textView = itemView.findViewById(R.id.menu_item); 55 | } 56 | 57 | public void setData(MenuItem menuItem) { 58 | this.menuItem = menuItem; 59 | if(menuItem.getDrawableResource() != 0 ) 60 | imageView.setImageResource(menuItem.getDrawableResource()); 61 | else imageView.setVisibility(View.INVISIBLE); 62 | 63 | textView.setText(menuItem.getTitle()); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @android:color/transparent 4 | @android:color/transparent 5 | @android:color/transparent 6 | 7 | #010101 8 | #e9eaed 9 | #999999 10 | #ebebeb 11 | #c4c4c4 12 | #f12033 13 | 14 | #c4c4c4 15 | #80c4c4c4 16 | @android:color/black 17 | 18 | #6555fd 19 | #faae55 20 | #37a7ef 21 | #6555fd 22 | #ff1d34be 23 | #ff228B22 24 | #FF6600 25 | #ffbe224e 26 | #dca926 27 | #000000 28 | #6cc4fc 29 | #fe566d 30 | #3682ff 31 | #7869c0 32 | #7d9ef1 33 | #dfb610 34 | #59d368 35 | #029d00 36 | 37 | 38 | 39 | @color/color_1 40 | @color/color_2 41 | @color/color_3 42 | @color/color_4 43 | @color/color_5 44 | @color/color_6 45 | @color/color_7 46 | @color/color_8 47 | @color/color_9 48 | @color/color_10 49 | @color/color_11 50 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/views/CustomViewPager.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.views; 2 | 3 | import android.content.Context; 4 | import android.support.v4.view.ViewPager; 5 | import android.util.AttributeSet; 6 | import android.view.MotionEvent; 7 | 8 | /* 9 | * This is a custom view pager class to enable/disable swiping f 10 | * unctionality in a viewPager 11 | * 12 | * */ 13 | public class CustomViewPager extends ViewPager { 14 | 15 | public enum SwipeDirection { 16 | ALL, LEFT, RIGHT, NONE; 17 | } 18 | 19 | private float initialXValue; 20 | private SwipeDirection direction; 21 | 22 | public CustomViewPager(Context context, AttributeSet attrs) { 23 | super(context, attrs); 24 | this.direction = SwipeDirection.ALL; 25 | } 26 | 27 | public void setCurrentItem() { 28 | this.setCurrentItem(); 29 | } 30 | 31 | @Override 32 | public boolean onTouchEvent(MotionEvent event) { 33 | if (this.IsSwipeAllowed(event)) { 34 | return super.onTouchEvent(event); 35 | } 36 | 37 | return false; 38 | } 39 | 40 | @Override 41 | public boolean onInterceptTouchEvent(MotionEvent event) { 42 | if (this.IsSwipeAllowed(event)) { 43 | return super.onInterceptTouchEvent(event); 44 | } 45 | return false; 46 | } 47 | 48 | private boolean IsSwipeAllowed(MotionEvent event) { 49 | if (this.direction == SwipeDirection.ALL) return true; 50 | 51 | if (direction == SwipeDirection.NONE)//disable any swipe 52 | return false; 53 | 54 | if (event.getAction() == MotionEvent.ACTION_DOWN) { 55 | initialXValue = event.getX(); 56 | return true; 57 | } 58 | 59 | if (event.getAction() == MotionEvent.ACTION_MOVE) { 60 | try { 61 | float diffX = event.getX() - initialXValue; 62 | if (diffX > 0 && direction == SwipeDirection.RIGHT) { 63 | // swipe from left to right detected 64 | return false; 65 | } else if (diffX < 0 && direction == SwipeDirection.LEFT) { 66 | // swipe from right to left detected 67 | return false; 68 | } 69 | } catch (Exception exception) { 70 | exception.printStackTrace(); 71 | } 72 | } 73 | 74 | return true; 75 | } 76 | 77 | public void setAllowedSwipeDirection(SwipeDirection direction) { 78 | this.direction = direction; 79 | } 80 | } -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | InShorts 3 | Search... 4 | 5 | Sort by Newest First 6 | Sort by Oldest First 7 | Filter by Category 8 | Filter by Publisher 9 | Favourites 10 | Offline Feed 11 | My Feed 12 | 13 | 14 | 15 | @drawable/ic_business 16 | @drawable/ic_tech 17 | @drawable/ic_business 18 | @drawable/ic_tech 19 | 20 | 21 | 22 | @string/sort_item_1 23 | @string/sort_item_2 24 | 25 | 26 | 27 | @string/filter_item_1 28 | @string/filter_item_2 29 | 30 | 31 | 32 | @string/menu_item_1 33 | @string/menu_item_2 34 | @string/menu_item_3 35 | 36 | 37 | 38 | @raw/ic_heart 39 | @raw/ic_download 40 | @drawable/ic_tech 41 | 42 | 43 | Oops...There seems to be an issue. Please try again 44 | There are no favourites at the moment 45 | There are no offline feeds at the moment 46 | Sorry! The page you were looking for cannot be found.\n\nThe page may no longer exist or may have moved to another web address.\n\nThank you 47 | Oops...You do not seem to have a proper internet connection. Please try again after connecting to a valid internet connection 48 | 49 | Open 50 | Close 51 | 52 | Added news to favourites 53 | Removed news from favourites 54 | Added news to Offline feed 55 | Removed news from Offline feed 56 | 57 | 58 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 13 | 14 | 15 | 19 | 20 | 26 | 27 | 40 | 41 | 42 | 43 | 44 | 54 | 55 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/activity/CustomWebViewActivity.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.activity; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.view.View; 6 | import android.webkit.WebChromeClient; 7 | import android.webkit.WebSettings; 8 | import android.webkit.WebView; 9 | 10 | import com.an.inshorts.R; 11 | import com.an.inshorts.model.MenuItem; 12 | import com.an.inshorts.utils.BaseUtils; 13 | import com.an.inshorts.utils.ConnectivityStatus; 14 | import com.an.inshorts.views.CustomWebViewClient; 15 | 16 | import static com.an.inshorts.BaseConstants.INTENT_CATEGORY_NAME; 17 | import static com.an.inshorts.BaseConstants.INTENT_URL; 18 | 19 | public class CustomWebViewActivity extends BaseActivity implements View.OnClickListener { 20 | 21 | private WebView webView; 22 | private View progressView; 23 | private String url; 24 | 25 | @Override 26 | protected void onCreate(@Nullable Bundle savedInstanceState) { 27 | super.onCreate(savedInstanceState); 28 | setContentView(R.layout.activity_webview); 29 | 30 | initToolbar(); 31 | updateToolbarTitle(getIntent().getStringExtra(INTENT_CATEGORY_NAME)); 32 | showShareBtn(); 33 | updateBackButton(); 34 | 35 | progressView = findViewById(R.id.progress_view); 36 | 37 | webView = (WebView) findViewById(R.id.webView); 38 | webView.getSettings().setJavaScriptEnabled(true); 39 | webView.getSettings().setLoadWithOverviewMode(true); 40 | webView.setWebViewClient(new CustomWebViewClient(webView, this)); 41 | webView.getSettings().setAppCachePath(getApplicationContext().getCacheDir().getAbsolutePath()); 42 | webView.getSettings().setAllowFileAccess(true); 43 | webView.getSettings().setAppCacheEnabled(true); 44 | webView.getSettings().setJavaScriptEnabled(true); 45 | 46 | int cacheMode = ConnectivityStatus.isConnected(this) ? WebSettings.LOAD_DEFAULT : WebSettings.LOAD_CACHE_ELSE_NETWORK; 47 | webView.getSettings().setCacheMode(cacheMode); 48 | 49 | webView.setWebChromeClient(new WebChromeClient() { 50 | public void onProgressChanged(WebView view, int progress) { 51 | if (progress >= 90) progressView.setVisibility(View.GONE); 52 | } 53 | }); 54 | 55 | url = getIntent().getStringExtra(INTENT_URL); 56 | webView.loadUrl(url); 57 | } 58 | 59 | @Override 60 | public void onMenuItemClick(MenuItem item) { 61 | 62 | } 63 | 64 | 65 | @Override 66 | public void onClick(View view) { 67 | if (view == getFilterBtn()) { 68 | BaseUtils.share(CustomWebViewActivity.this, url); 69 | 70 | } else if(view == getMenuBtn()) { 71 | onBackPressed(); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/adapter/FeedListAdapter.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.adapter; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.TextView; 9 | 10 | import com.an.inshorts.R; 11 | import com.an.inshorts.model.Feed; 12 | 13 | import java.util.List; 14 | 15 | public class FeedListAdapter extends RecyclerView.Adapter { 16 | 17 | private Context context; 18 | private List feedList; 19 | public FeedListAdapter(Context context, List feedList) { 20 | this.context = context; 21 | this.feedList = feedList; 22 | } 23 | 24 | @Override 25 | public CustomViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 26 | View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.fragment_list_item, parent, false); 27 | return new CustomViewHolder(itemView); 28 | } 29 | 30 | @Override 31 | public void onBindViewHolder(CustomViewHolder holder, int position) { 32 | Feed feed = feedList.get(position); 33 | holder.headlineTxt.setText(feed.getTitle()); 34 | holder.sourceTxt.setText(feed.getPublisher()); 35 | 36 | if(position == 0) { 37 | holder.headlineTxt.setLineSpacing(new Float(context.getResources().getDimension(R.dimen.margin)), 1f); 38 | holder.headlineTxt.setTextSize(new Float(context.getResources().getDimension(R.dimen.font_xxxxsmall))); 39 | } else { 40 | holder.rootView.setPadding(new Float(context.getResources().getDimension(R.dimen.padding_large)).intValue(), 41 | new Float(context.getResources().getDimension(R.dimen.padding_xsmall)).intValue(), 42 | new Float(context.getResources().getDimension(R.dimen.padding_large)).intValue(), 43 | new Float(context.getResources().getDimension(R.dimen.padding_xsmall)).intValue()); 44 | holder.headlineTxt.setLineSpacing(3f, 1.5f); 45 | holder.headlineTxt.setTextSize(new Float(context.getResources().getDimension(R.dimen.font_xxxxxsmall))); 46 | } 47 | } 48 | 49 | @Override 50 | public int getItemCount() { 51 | return feedList.size(); 52 | } 53 | 54 | public class CustomViewHolder extends RecyclerView.ViewHolder { 55 | 56 | private View rootView; 57 | private TextView headlineTxt; 58 | private TextView sourceTxt; 59 | public CustomViewHolder(View itemView) { 60 | super(itemView); 61 | rootView = itemView.findViewById(R.id.root_view); 62 | headlineTxt = itemView.findViewById(R.id.feed_headlines); 63 | sourceTxt = itemView.findViewById(R.id.feed_source); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/res/layout/bottom_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 21 | 22 | 39 | 40 | 41 | 52 | 53 | 54 | 55 | 56 | 64 | 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/rest/RESTCacheRequest.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.rest; 2 | 3 | import com.android.volley.Cache; 4 | import com.android.volley.NetworkResponse; 5 | import com.android.volley.Request; 6 | import com.android.volley.Response; 7 | import com.android.volley.VolleyError; 8 | import com.android.volley.toolbox.HttpHeaderParser; 9 | 10 | import java.io.UnsupportedEncodingException; 11 | 12 | public class RESTCacheRequest extends Request { 13 | private final Response.Listener mListener; 14 | private final Response.ErrorListener mErrorListener; 15 | 16 | public RESTCacheRequest(int method, String url, Response.Listener listener, Response.ErrorListener errorListener) { 17 | super(method, url, errorListener); 18 | this.mListener = listener; 19 | this.mErrorListener = errorListener; 20 | } 21 | 22 | 23 | @Override 24 | protected Response parseNetworkResponse(NetworkResponse response) { 25 | Cache.Entry cacheEntry = HttpHeaderParser.parseCacheHeaders(response); 26 | if (cacheEntry == null) { 27 | cacheEntry = new Cache.Entry(); 28 | } 29 | final long cacheHitButRefreshed = 3 * 60 * 1000; // in 3 minutes cache will be hit, but also refreshed on background 30 | final long cacheExpired = 6 * 60 * 60 * 1000; // in 6 hours this cache entry expires completely 31 | long now = System.currentTimeMillis(); 32 | final long softExpire = now + cacheHitButRefreshed; 33 | final long ttl = now + cacheExpired; 34 | cacheEntry.data = response.data; 35 | cacheEntry.softTtl = softExpire; 36 | cacheEntry.ttl = ttl; 37 | String headerValue; 38 | headerValue = response.headers.get("Date"); 39 | if (headerValue != null) { 40 | cacheEntry.serverDate = HttpHeaderParser.parseDateAsEpoch(headerValue); 41 | } 42 | headerValue = response.headers.get("Last-Modified"); 43 | if (headerValue != null) { 44 | cacheEntry.lastModified = HttpHeaderParser.parseDateAsEpoch(headerValue); 45 | } 46 | cacheEntry.responseHeaders = response.headers; 47 | try { 48 | String jsonString =new String(response.data, HttpHeaderParser.parseCharset(response.headers)); 49 | return Response.success(jsonString, HttpHeaderParser.parseCacheHeaders(response)); 50 | 51 | } catch (UnsupportedEncodingException e) { 52 | e.printStackTrace(); 53 | } 54 | return null; 55 | } 56 | 57 | @Override 58 | protected void deliverResponse(String response) { 59 | mListener.onResponse(response); 60 | } 61 | 62 | @Override 63 | protected VolleyError parseNetworkError(VolleyError volleyError) { 64 | return super.parseNetworkError(volleyError); 65 | } 66 | 67 | @Override 68 | public void deliverError(VolleyError error) { 69 | mErrorListener.onErrorResponse(error); 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/db/FeedDb.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.db; 2 | 3 | import com.an.inshorts.model.Feed; 4 | import com.an.inshorts.utils.BaseUtils; 5 | 6 | import java.io.Serializable; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | public class FeedDb implements Serializable { 12 | 13 | private static FeedDb instance; 14 | public static FeedDb getInstance() { 15 | if(instance == null) readFromDisk(); 16 | return instance; 17 | } 18 | 19 | private static void readFromDisk() { 20 | instance = (FeedDb) BaseUtils.readObjectFromDisk(); 21 | if (instance == null) { 22 | instance = new FeedDb(); 23 | saveToDisk(); 24 | } 25 | } 26 | 27 | private static void saveToDisk() { 28 | BaseUtils.writeObjectToDisk(instance); 29 | } 30 | 31 | private List feedList; 32 | private Map favFeeds; 33 | private Map offlineFeeds; 34 | 35 | public Map getFavFeeds() { 36 | if(favFeeds == null) favFeeds = new HashMap<>(); 37 | return favFeeds; 38 | } 39 | 40 | public void setFavFeeds(Map favFeeds) { 41 | this.favFeeds = favFeeds; 42 | saveToDisk(); 43 | } 44 | 45 | public void addToFavFeeds(Feed feed) { 46 | Map faves = getFavFeeds(); 47 | faves.put(feed.getId(), feed); 48 | setFavFeeds(faves); 49 | } 50 | 51 | public void removeFromFavFeeds(Feed feed) { 52 | Map faves = getFavFeeds(); 53 | if(faves.containsKey(feed.getId())) 54 | faves.remove(feed.getId()); 55 | setFavFeeds(faves); 56 | } 57 | 58 | public boolean isFavourite(long feedId) { 59 | return getFavFeeds().containsKey(feedId); 60 | } 61 | 62 | 63 | public Map getOfflineFeeds() { 64 | if(offlineFeeds == null) offlineFeeds = new HashMap<>(); 65 | return offlineFeeds; 66 | } 67 | 68 | public void setOfflineFeeds(Map offlineFeeds) { 69 | this.offlineFeeds = offlineFeeds; 70 | saveToDisk(); 71 | } 72 | 73 | public void addToOfflineFeeds(Feed feed) { 74 | Map offlineList = getOfflineFeeds(); 75 | offlineList.put(feed.getId(), feed); 76 | setOfflineFeeds(offlineList); 77 | } 78 | 79 | public void removeFromOfflineFeeds(Feed feed) { 80 | Map offlineMap = getOfflineFeeds(); 81 | if(offlineMap.containsKey(feed.getId())) 82 | offlineMap.remove(feed.getId()); 83 | setOfflineFeeds(offlineMap); 84 | } 85 | 86 | public boolean isOffline(long feedId) { 87 | return getOfflineFeeds().containsKey(feedId); 88 | } 89 | 90 | public List getFeedList() { 91 | return feedList; 92 | } 93 | 94 | public void setFeedList(List feedList) { 95 | this.feedList = feedList; 96 | saveToDisk(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/src/main/res/layout/view_toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 23 | 24 | 25 | 28 | 29 | 39 | 40 | 41 | 51 | 52 | 53 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /app/src/main/res/raw/test_feed.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "ID": 1, 3 | "TITLE": "Fed official says weak data caused by weather, should not slow taper", 4 | "URL": "http://www.latimes.com/business/money/la-fi-mo-federal-reserve-plosser-stimulus-economy-20140310,0,1312750.story\\?track=rss", 5 | "PUBLISHER": "Los Angeles Times", 6 | "CATEGORY": "b", 7 | "HOSTNAME": "www.latimes.com", 8 | "TIMESTAMP": 1394470370698 9 | }, 10 | { 11 | "ID": 2, 12 | "TITLE": "Feds Charles Plosser sees high bar for change in pace of tapering", 13 | "URL": "http://www.livemint.com/Politics/H2EvwJSK2VE6OF7iK1g3PP/Feds-Charles-Plosser-sees-high-bar-for-change-in-pace-of-ta.html", 14 | "PUBLISHER": "Livemint", 15 | "CATEGORY": "b", 16 | "HOSTNAME": "www.livemint.com", 17 | "TIMESTAMP": 1394470371207 18 | }, 19 | { 20 | "ID": 3, 21 | "TITLE": "US open: Stocks fall after Fed official hints at accelerated tapering", 22 | "URL": "http://www.ifamagazine.com/news/us-open-stocks-fall-after-fed-official-hints-at-accelerated-tapering-294436", 23 | "PUBLISHER": "IFA Magazine", 24 | "CATEGORY": "b", 25 | "HOSTNAME": "www.ifamagazine.com", 26 | "TIMESTAMP": 1394470371550 27 | }, 28 | { 29 | "ID": 998, 30 | "TITLE": "Titanfall On Xbox One Likely To Get Improved Resolution After Launch", 31 | "URL": "http://thekoalition.com/2014/03/titanfall-on-xbox-one-likely-to-get-improved-resolution-after-launch/", 32 | "PUBLISHER": "The Koalition", 33 | "CATEGORY": "t", 34 | "HOSTNAME": "thekoalition.com", 35 | "TIMESTAMP": 1394493719614 36 | }, 37 | { 38 | "ID": 999, 39 | "TITLE": "'Titanfall' Release Date Nears: Twitter Predicts It Will Outsell 'Battlefield 4', Will ...", 40 | "URL": "http://www.ibtimes.com/titanfall-release-date-nears-twitter-predicts-it-will-outsell-battlefield-4-will-compete-call-duty", 41 | "PUBLISHER": "International Business Times", 42 | "CATEGORY": "t", 43 | "HOSTNAME": "www.ibtimes.com", 44 | "TIMESTAMP": 1394493719773 45 | }, 46 | { 47 | "ID": 1000, 48 | "TITLE": "Buy Titanfall And 12 Month Xbox Live Membership At Target And Save $30", 49 | "URL": "http://www.justpushstart.com/2014/03/buy-titanfall-12-month-xbox-live-membership-target-save-30/", 50 | "PUBLISHER": "Just Push Start", 51 | "CATEGORY": "t", 52 | "HOSTNAME": "www.justpushstart.com", 53 | "TIMESTAMP": 1394493720128 54 | }, 55 | { 56 | "ID": 1000, 57 | "TITLE": "Buy Titanfall And 12 Month Xbox Live Membership At Target And Save $30", 58 | "URL": "http://www.justpushstart.com/2014/03/buy-titanfall-12-month-xbox-live-membership-target-save-30/", 59 | "PUBLISHER": "Just Push Start", 60 | "CATEGORY": "t", 61 | "HOSTNAME": "www.justpushstart.com", 62 | "TIMESTAMP": 1394493720128 63 | }, 64 | { 65 | "ID": 1000, 66 | "TITLE": "Buy Titanfall And 12 Month Xbox Live Membership At Target And Save $30", 67 | "URL": "http://www.justpushstart.com/2014/03/buy-titanfall-12-month-xbox-live-membership-target-save-30/", 68 | "PUBLISHER": "Just Push Start", 69 | "CATEGORY": "t", 70 | "HOSTNAME": "www.justpushstart.com", 71 | "TIMESTAMP": 1394493720128 72 | } 73 | ] -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/views/MenuCreator.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.views; 2 | 3 | 4 | import android.os.Bundle; 5 | import android.support.annotation.NonNull; 6 | import android.support.annotation.Nullable; 7 | import android.support.design.widget.NavigationView; 8 | import android.support.v4.view.GravityCompat; 9 | import android.support.v4.widget.DrawerLayout; 10 | import android.support.v7.app.AppCompatActivity; 11 | import android.support.v7.widget.LinearLayoutManager; 12 | import android.support.v7.widget.RecyclerView; 13 | import android.view.Gravity; 14 | import android.view.MenuItem; 15 | import android.view.View; 16 | 17 | import com.an.inshorts.R; 18 | import com.an.inshorts.adapter.MenuItemAdapter; 19 | import com.an.inshorts.listener.MenuItemListener; 20 | import com.an.inshorts.utils.BaseUtils; 21 | 22 | import java.util.List; 23 | 24 | public class MenuCreator extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener, 25 | RecyclerItemClickListener.OnItemClickListener { 26 | 27 | private DrawerLayout mDrawerLayout; 28 | private RecyclerView mDrawerList; 29 | private NavigationView navigationView; 30 | 31 | private MenuItemAdapter menuItemAdapter; 32 | private MenuItemListener listener; 33 | 34 | @Override 35 | protected void onCreate(@Nullable Bundle savedInstanceState) { 36 | super.onCreate(savedInstanceState); 37 | } 38 | 39 | protected void initNavigationDrawer(MenuItemListener listener) { 40 | this.listener = listener; 41 | mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); 42 | mDrawerList = (RecyclerView) findViewById(R.id.bottom_list); 43 | navigationView = (NavigationView) findViewById(R.id.nav_container); 44 | navigationView.setNavigationItemSelectedListener(this); 45 | 46 | setUpMenuList(); 47 | } 48 | 49 | private void setUpMenuList() { 50 | mDrawerList.setLayoutManager(new LinearLayoutManager(this)); 51 | mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); 52 | 53 | List menuItems = BaseUtils.getMenuItems(this); 54 | menuItemAdapter = new MenuItemAdapter(menuItems); 55 | mDrawerList.setAdapter(menuItemAdapter); 56 | mDrawerList.addOnItemTouchListener(new RecyclerItemClickListener(this, this)); 57 | } 58 | 59 | @Override 60 | public boolean onNavigationItemSelected(@NonNull MenuItem item) { 61 | return false; 62 | } 63 | 64 | protected void dismissDrawer() { 65 | mDrawerLayout.closeDrawer(Gravity.LEFT); 66 | } 67 | 68 | protected void openDrawer() { 69 | mDrawerLayout.openDrawer(Gravity.LEFT); 70 | } 71 | 72 | protected boolean isDrawerOpened() { 73 | return mDrawerLayout.isDrawerOpen(GravityCompat.START); 74 | } 75 | 76 | protected void toggleDrawer() { 77 | if(isDrawerOpened()) dismissDrawer(); 78 | else openDrawer(); 79 | } 80 | 81 | @Override 82 | public void onItemClick(View childView, int position) { 83 | dismissDrawer(); 84 | listener.onMenuItemClick(menuItemAdapter.getItem(position)); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/fragment/MainFragment.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.fragment; 2 | 3 | import android.content.res.TypedArray; 4 | import android.os.Bundle; 5 | import android.support.annotation.Nullable; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.ImageView; 12 | import android.widget.TextView; 13 | 14 | import com.an.inshorts.R; 15 | import com.an.inshorts.adapter.FeedListAdapter; 16 | import com.an.inshorts.model.Feed; 17 | import com.an.inshorts.utils.NavigatorUtils; 18 | import com.an.inshorts.views.RecyclerItemClickListener; 19 | 20 | import java.io.Serializable; 21 | import java.util.List; 22 | 23 | public class MainFragment extends BaseFragment implements RecyclerItemClickListener.OnItemClickListener { 24 | 25 | private String categoryName; 26 | private List categories; 27 | private int position; 28 | 29 | private TextView categoryTxt; 30 | private ImageView categoryImg; 31 | 32 | private RecyclerView recyclerView; 33 | private FeedListAdapter adapter; 34 | 35 | public static MainFragment newInstance(int position, String categoryName, List categories) { 36 | MainFragment fragment = new MainFragment(); 37 | Bundle args = new Bundle(); 38 | args.putString(INTENT_CATEGORY_NAME, categoryName); 39 | args.putInt("position", position); 40 | args.putSerializable(INTENT_CATEGORIES, (Serializable) categories); 41 | fragment.setArguments(args); 42 | return fragment; 43 | } 44 | 45 | @Override 46 | public void onCreate(@Nullable Bundle savedInstanceState) { 47 | super.onCreate(savedInstanceState); 48 | categories = (List) getArguments().getSerializable(INTENT_CATEGORIES); 49 | categoryName = getArguments().getString(INTENT_CATEGORY_NAME); 50 | position = getArguments().getInt("position"); 51 | } 52 | 53 | @Nullable 54 | @Override 55 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 56 | View rootView = inflater.inflate(R.layout.fragment_main, null); 57 | 58 | recyclerView = rootView.findViewById(R.id.feed_list); 59 | recyclerView.setLayoutManager(new LinearLayoutManager(activity)); 60 | recyclerView.addOnItemTouchListener(new RecyclerItemClickListener(activity, this)); 61 | 62 | adapter = new FeedListAdapter(activity, categories); 63 | recyclerView.setAdapter(adapter); 64 | 65 | categoryTxt = rootView.findViewById(R.id.category_name); 66 | categoryTxt.setText(CATEGORY.get(categoryName) == null ? categoryName : CATEGORY.get(categoryName)); 67 | 68 | categoryImg = rootView.findViewById(R.id.category_img); 69 | 70 | if(CATEGORY.get(categoryName) != null) { 71 | TypedArray categoryIcons = activity.getResources().obtainTypedArray(R.array.category_icons); 72 | if(position < categoryIcons.length()) { 73 | int resourceId = categoryIcons.getResourceId(position, -1); 74 | categoryImg.setImageResource(resourceId); 75 | categoryIcons.recycle(); 76 | } else categoryImg.setVisibility(View.INVISIBLE); 77 | } else categoryImg.setVisibility(View.GONE); 78 | 79 | 80 | return rootView; 81 | } 82 | 83 | public List getFeeds() { 84 | return categories; 85 | } 86 | 87 | 88 | @Override 89 | public void onItemClick(View childView, int position) { 90 | NavigatorUtils.openFeedScreen(activity, categoryName, categories); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/activity/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.activity; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v7.widget.Toolbar; 6 | import android.view.View; 7 | import android.widget.ImageView; 8 | import android.widget.TextView; 9 | 10 | import com.an.inshorts.BaseConstants; 11 | import com.an.inshorts.R; 12 | import com.an.inshorts.dialogs.BottomSheetHelper; 13 | import com.an.inshorts.dialogs.CustomBottomSheetDialog; 14 | import com.an.inshorts.listener.MenuItemListener; 15 | import com.an.inshorts.utils.BaseUtils; 16 | import com.an.inshorts.views.MenuCreator; 17 | 18 | import java.util.Arrays; 19 | 20 | public abstract class BaseActivity extends MenuCreator implements View.OnClickListener, MenuItemListener, BaseConstants { 21 | 22 | protected Toolbar toolbar; 23 | protected TextView toolbarTitle; 24 | private ImageView menuBtn; 25 | private ImageView filterBtn; 26 | 27 | @Override 28 | protected void onCreate(@Nullable Bundle savedInstanceState) { 29 | super.onCreate(savedInstanceState); 30 | } 31 | 32 | protected void initToolbar() { 33 | toolbar = (Toolbar) findViewById(R.id.tool_bar); 34 | toolbarTitle = toolbar.findViewById(R.id.toolbar_title); 35 | menuBtn = toolbar.findViewById(R.id.btn_back); 36 | menuBtn.setOnClickListener(this); 37 | filterBtn = toolbar.findViewById(R.id.btn_filter); 38 | filterBtn.setOnClickListener(this); 39 | } 40 | 41 | protected void disableFilterBtn() { 42 | filterBtn.setEnabled(false); 43 | } 44 | 45 | protected void enableFilterBtn() { 46 | filterBtn.setEnabled(true); 47 | } 48 | 49 | protected void updateBackButton() { 50 | menuBtn.setTag(R.drawable.ic_back); 51 | menuBtn.setImageResource(R.drawable.ic_back); 52 | } 53 | 54 | protected void updateMenuButton() { 55 | menuBtn.setTag(R.drawable.ic_side_menu); 56 | menuBtn.setImageResource(R.drawable.ic_side_menu); 57 | initNavigationDrawer(BaseActivity.this); 58 | } 59 | 60 | protected void updateToolbarTitle(String text) { 61 | toolbarTitle.setText(text); 62 | toolbarTitle.setTextSize(new Float(getResources().getDimension(R.dimen.font_xxxsmall))); 63 | } 64 | 65 | protected void showShareBtn() { 66 | filterBtn.setTag(R.drawable.ic_share); 67 | filterBtn.setImageResource(R.drawable.ic_share); 68 | } 69 | 70 | protected void displaySortBtn() { 71 | filterBtn.setTag(R.drawable.ic_sort); 72 | filterBtn.setImageResource(R.drawable.ic_sort); 73 | } 74 | 75 | protected void displayFilterBtn() { 76 | filterBtn.setTag(R.drawable.ic_filters); 77 | filterBtn.setImageResource(R.drawable.ic_filters); 78 | } 79 | 80 | protected ImageView getFilterBtn() { 81 | return filterBtn; 82 | } 83 | 84 | protected ImageView getMenuBtn() { 85 | return menuBtn; 86 | } 87 | 88 | @Override 89 | public void onClick(View view) { 90 | if(view == filterBtn) { 91 | int tag = (Integer) filterBtn.getTag(); 92 | switch (tag) { 93 | case R.drawable.ic_sort : 94 | BottomSheetHelper.getInstance().showBottomSheet(BaseActivity.this, 95 | BaseUtils.addMenuItems(Arrays.asList(getResources().getStringArray(R.array.sort_items))), this); 96 | break; 97 | 98 | case R.drawable.ic_filters : 99 | BottomSheetHelper.getInstance().showBottomSheet(BaseActivity.this, 100 | BaseUtils.addMenuItems(Arrays.asList(getResources().getStringArray(R.array.filter_items))), this); 101 | break; 102 | } 103 | 104 | } else if(view == menuBtn) { 105 | int tag = (Integer) menuBtn.getTag(); 106 | switch (tag) { 107 | case R.drawable.ic_side_menu : 108 | toggleDrawer(); 109 | break; 110 | 111 | case R.drawable.ic_back : 112 | onBackPressed(); 113 | break; 114 | } 115 | 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/res/layout/feed_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 26 | 27 | 28 | 39 | 40 | 41 | 47 | 48 | 49 | 68 | 69 | 70 | 71 | 90 | 91 | 92 | 93 | 94 | 100 | 101 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/activity/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.activity; 2 | 3 | import android.animation.ArgbEvaluator; 4 | import android.graphics.PorterDuff; 5 | import android.graphics.PorterDuffColorFilter; 6 | import android.graphics.drawable.Drawable; 7 | import android.os.Bundle; 8 | import android.support.annotation.Nullable; 9 | import android.support.v4.content.ContextCompat; 10 | import android.support.v4.view.ViewPager; 11 | import android.view.View; 12 | import android.widget.ImageView; 13 | 14 | import com.an.inshorts.R; 15 | import com.an.inshorts.adapter.MainPagerAdapter; 16 | import com.an.inshorts.fragment.MainFragment; 17 | import com.an.inshorts.listener.OnFeedChangeListener; 18 | import com.an.inshorts.model.Feed; 19 | import com.an.inshorts.model.MenuItem; 20 | import com.an.inshorts.service.FeedService; 21 | import com.an.inshorts.service.FeedServiceImpl; 22 | import com.an.inshorts.utils.BaseUtils; 23 | import com.an.inshorts.views.CustomPageTransformer; 24 | import com.an.inshorts.views.CustomViewPager; 25 | 26 | import java.util.ArrayList; 27 | import java.util.List; 28 | import java.util.Map; 29 | 30 | 31 | public class MainActivity extends BaseActivity implements OnFeedChangeListener, ViewPager.OnPageChangeListener { 32 | 33 | private ImageView imgCategory; 34 | private View progressView; 35 | 36 | private CustomViewPager viewPager; 37 | private MainPagerAdapter pagerAdapter; 38 | 39 | private List fragments = new ArrayList<>(); 40 | 41 | private int [] colors; 42 | private ArgbEvaluator argbEvaluator = new ArgbEvaluator(); 43 | 44 | private FeedService feedService; 45 | 46 | @Override 47 | protected void onCreate(@Nullable Bundle savedInstanceState) { 48 | super.onCreate(savedInstanceState); 49 | setContentView(R.layout.activity_main); 50 | 51 | initToolbar(); 52 | updateMenuButton(); 53 | displayFilterBtn(); 54 | disableFilterBtn(); 55 | updateToolbarTitle(getString(R.string.app_name)); 56 | 57 | feedService = new FeedServiceImpl(this, this); 58 | feedService.handleAction(ACTION_TYPE_GET_FEED, null, false); 59 | 60 | progressView = findViewById(R.id.progress_view); 61 | progressView.setVisibility(View.VISIBLE); 62 | viewPager = (CustomViewPager) findViewById(R.id.viewpager); 63 | imgCategory = (ImageView) findViewById(R.id.img_category); 64 | 65 | pagerAdapter = new MainPagerAdapter(getSupportFragmentManager(), fragments); 66 | viewPager.setAdapter(pagerAdapter); 67 | viewPager.setPageTransformer(true, new CustomPageTransformer()); 68 | 69 | colors = getResources().getIntArray(R.array.colors); 70 | viewPager.addOnPageChangeListener(this); 71 | } 72 | 73 | @Override 74 | public void onMenuItemClick(MenuItem item) { 75 | MainFragment fragment = (MainFragment) pagerAdapter.getItem(viewPager.getCurrentItem()); 76 | Map> filterFeed = feedService.handleMenuItemClick(item.getTitle(), fragment.getFeeds()); 77 | if(filterFeed != null) refreshData(filterFeed); 78 | } 79 | 80 | private void refreshData(Map> feeds) { 81 | progressView.setVisibility(View.GONE); 82 | enableFilterBtn(); 83 | 84 | int i = 0; 85 | if(pagerAdapter != null) pagerAdapter.removeFragments(); 86 | for(Map.Entry> entry : feeds.entrySet()) { 87 | List subList = new ArrayList<>(); 88 | if(entry.getValue().size() <= PAGE_SIZE) subList.addAll(entry.getValue()); 89 | else subList.addAll(entry.getValue().subList(0, PAGE_SIZE)); 90 | pagerAdapter.addFragment(MainFragment.newInstance(i, entry.getKey(), subList)); 91 | i++; 92 | } 93 | pagerAdapter.refreshAdapter(); 94 | viewPager.setCurrentItem(0); 95 | } 96 | 97 | @Override 98 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 99 | if(position +1 < colors.length) { 100 | Drawable mDrawable = ContextCompat.getDrawable(MainActivity.this, R.drawable.ic_category_3); 101 | mDrawable.setColorFilter(new PorterDuffColorFilter((Integer) argbEvaluator.evaluate(positionOffset, colors[position], colors[position + 1]), PorterDuff.Mode.SRC_IN)); 102 | imgCategory.setImageDrawable(mDrawable); 103 | } 104 | } 105 | 106 | @Override 107 | public void onPageSelected(int position) { 108 | 109 | } 110 | 111 | @Override 112 | public void onPageScrollStateChanged(int state) { 113 | 114 | } 115 | 116 | @Override 117 | public void showError(String message) { 118 | BaseUtils.showSnackBar(message, findViewById(R.id.root_view)); 119 | } 120 | 121 | @Override 122 | public void refreshFeed(Map> data) { 123 | refreshData(data); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/service/DataServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.service; 2 | 3 | 4 | import android.content.Context; 5 | 6 | import com.an.inshorts.BaseConstants; 7 | import com.an.inshorts.db.DbExecutorService; 8 | import com.an.inshorts.db.FeedDbTask; 9 | import com.an.inshorts.model.Feed; 10 | import com.an.inshorts.utils.CollectionUtils; 11 | 12 | import java.util.ArrayList; 13 | import java.util.HashMap; 14 | import java.util.Iterator; 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | 19 | /* 20 | * Class to handle/access data 21 | * 22 | * */ 23 | public class DataServiceImpl extends AbstractServiceImpl implements DataService { 24 | 25 | 26 | public DataServiceImpl(Context context) { 27 | super(context); 28 | } 29 | 30 | @Override 31 | public List sortFeedAsc(List data) { 32 | return CollectionUtils.sortFeedAsc(data); 33 | } 34 | 35 | @Override 36 | public List sortFeedDesc(List data) { 37 | return CollectionUtils.sortFeedDesc(data); 38 | } 39 | 40 | @Override 41 | public Map> filterByCategory(List data) { 42 | Map> finalMap = new HashMap<>(); 43 | if(data == null) data = getFeeds(); 44 | for(Feed feed : data) { 45 | if (!finalMap.containsKey(feed.getCategory())) { 46 | List list = new ArrayList(); 47 | list.add(feed); 48 | 49 | finalMap.put(feed.getCategory(), list); 50 | } else { 51 | finalMap.get(feed.getCategory()).add(feed); 52 | } 53 | } 54 | return finalMap; 55 | } 56 | 57 | @Override 58 | public Map> filterByPublisher(List data) { 59 | Map> finalMap = new HashMap<>(); 60 | if(data == null) data = getFeeds(); 61 | for(Feed feed : data) { 62 | if (!finalMap.containsKey(feed.getPublisher())) { 63 | List list = new ArrayList(); 64 | list.add(feed); 65 | 66 | finalMap.put(feed.getPublisher(), list); 67 | } else { 68 | finalMap.get(feed.getPublisher()).add(feed); 69 | } 70 | } 71 | return finalMap; 72 | } 73 | 74 | @Override 75 | public List getFeedsByCategory(String name, int limit) { 76 | List finalList = new ArrayList<>(); 77 | List feeds = getFeeds().subList(limit, (limit+PAGE_SIZE)); 78 | for(Feed feed : feeds) { 79 | if(name.equalsIgnoreCase(feed.getCategory())) 80 | finalList.add(feed); 81 | } 82 | return finalList; 83 | } 84 | 85 | @Override 86 | public List getFeedsByPublisher(String name, int limit) { 87 | List finalList = new ArrayList<>(); 88 | List feeds = getFeeds().subList(limit, (limit+PAGE_SIZE)); 89 | for(Feed feed : feeds) { 90 | if(name.equalsIgnoreCase(feed.getPublisher())) 91 | finalList.add(feed); 92 | } 93 | return finalList; 94 | } 95 | 96 | 97 | @Override 98 | public List getFavouriteFeeds() { 99 | List feeds = new ArrayList<>(); 100 | Map faves = feedModule.getFavourites(); 101 | 102 | Iterator> iterator = faves.entrySet().iterator(); 103 | while (iterator.hasNext()) { 104 | Map.Entry entry = iterator.next(); 105 | feeds.add(entry.getValue()); 106 | } 107 | return feeds; 108 | } 109 | 110 | 111 | 112 | @Override 113 | public List getOfflineFeeds() { 114 | List feeds = new ArrayList<>(); 115 | Map faves = feedModule.getOfflineFeeds(); 116 | 117 | Iterator> iterator = faves.entrySet().iterator(); 118 | while (iterator.hasNext()) { 119 | Map.Entry entry = iterator.next(); 120 | feeds.add(entry.getValue()); 121 | } 122 | return feeds; 123 | } 124 | 125 | @Override 126 | public List getFeeds() { 127 | return feedModule.getFeedsFromDb(); 128 | } 129 | 130 | @Override 131 | public void addFeedToDb(List feeds) { 132 | DbExecutorService.submit(new FeedDbTask(context, BaseConstants.TYPE_ADD_FEED, feeds)); 133 | } 134 | 135 | @Override 136 | public void addToFavourites(Feed feed) { 137 | DbExecutorService.submit(new FeedDbTask(context, BaseConstants.TYPE_ADD_FAVOURITES, feed)); 138 | } 139 | 140 | @Override 141 | public void removeFromFavourites(Feed feed) { 142 | DbExecutorService.submit(new FeedDbTask(context, BaseConstants.TYPE_REMOVE_FAVOURITES, feed)); 143 | } 144 | 145 | @Override 146 | public void addToOfflineFeed(Feed feed) { 147 | DbExecutorService.submit(new FeedDbTask(context, BaseConstants.TYPE_ADD_OFFLINE, feed)); 148 | } 149 | 150 | @Override 151 | public void removeFromOfflineFeed(Feed feed) { 152 | DbExecutorService.submit(new FeedDbTask(context, BaseConstants.TYPE_REMOVE_OFFLINE, feed)); 153 | } 154 | 155 | @Override 156 | public boolean isAddedToFavourite(Long id) { 157 | return feedModule.isFavourite(id); 158 | } 159 | 160 | @Override 161 | public boolean isAvailableOffline(Long id) { 162 | return feedModule.isOffline(id); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/utils/BaseUtils.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.utils; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.res.TypedArray; 7 | import android.support.design.widget.Snackbar; 8 | import android.view.View; 9 | import android.widget.Toast; 10 | 11 | import com.an.inshorts.BaseConstants; 12 | import com.an.inshorts.R; 13 | import com.an.inshorts.model.Feed; 14 | import com.an.inshorts.model.MenuItem; 15 | import com.google.gson.Gson; 16 | import com.google.gson.reflect.TypeToken; 17 | 18 | import java.io.BufferedReader; 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | import java.io.InputStreamReader; 22 | import java.lang.reflect.Type; 23 | import java.util.ArrayList; 24 | import java.util.Arrays; 25 | import java.util.List; 26 | 27 | public class BaseUtils implements BaseConstants { 28 | 29 | /* 30 | * Method to display SnackBar 31 | * implementation 32 | * */ 33 | 34 | public static void showSnackBar(String message, 35 | View view) { 36 | Snackbar.make(view, message,Snackbar.LENGTH_LONG).show(); 37 | } 38 | 39 | 40 | public static void showToast(Context context, 41 | String message) { 42 | Toast.makeText(context, message, Toast.LENGTH_LONG).show(); 43 | } 44 | 45 | 46 | /* 47 | * Method to read json file from raw folder 48 | * & return a string value 49 | * */ 50 | 51 | public static String getJSONStringFromRaw(Context context, int rawId) { 52 | 53 | InputStream content = context.getResources().openRawResource(rawId); 54 | BufferedReader buffer = new BufferedReader(new InputStreamReader(content)); 55 | String respString = ""; 56 | try { 57 | String s = ""; 58 | while ((s = buffer.readLine()) != null) { 59 | respString += s; 60 | } 61 | } catch (IOException e) { 62 | e.printStackTrace(); 63 | } 64 | return respString; 65 | } 66 | 67 | 68 | public static List loadDummyData(Context context) { 69 | String requestJson = getJSONStringFromRaw(context, R.raw.test_feed); 70 | Type listType = new TypeToken>() {}.getType(); 71 | List feeds = new Gson().fromJson(requestJson, listType); 72 | return feeds; 73 | } 74 | 75 | 76 | /* 77 | * Method to read the json string from response 78 | * The string is converted into a list using gson library 79 | * */ 80 | 81 | public static List loadFeedData(String requestJson) { 82 | Type listType = new TypeToken>() {}.getType(); 83 | List feeds = new Gson().fromJson(requestJson, listType); 84 | return feeds; 85 | } 86 | 87 | public static List loadMenuItems(Context context, int raw) { 88 | String menuJson = getJSONStringFromRaw(context, raw); 89 | Type listType = new TypeToken>() {}.getType(); 90 | List menuItems = new Gson().fromJson(menuJson, listType); 91 | return menuItems; 92 | } 93 | 94 | public static List addMenuItems(List items) { 95 | List menuItems = new ArrayList<>(); 96 | for(String s : items) { 97 | MenuItem menuItem = new MenuItem(0, s); 98 | menuItems.add(menuItem); 99 | } 100 | return menuItems; 101 | } 102 | 103 | public static String getCurrentGroup(String name) { 104 | return CATEGORY.get(name) == null ? FILTER_BY_PUBLISHER : FILTER_BY_CATEGORY; 105 | } 106 | 107 | 108 | public static List getMenuItems(Context context) { 109 | List list = new ArrayList<>(); 110 | 111 | List menuTitles = Arrays.asList(context.getResources().getStringArray(R.array.menu_items)); 112 | TypedArray menuIcons = context.getResources().obtainTypedArray(R.array.menu_icons); 113 | 114 | for(int i = 0; i< menuTitles.size(); i++) { 115 | MenuItem slideMenuItem = new MenuItem(menuIcons.getResourceId(i, -1), menuTitles.get(i)); 116 | list.add(slideMenuItem); 117 | } 118 | menuIcons.recycle(); 119 | return list; 120 | } 121 | 122 | /* 123 | * You can use this method to store the 124 | * request response from your local cache 125 | * */ 126 | 127 | public static void writeObjectToDisk(final Object object) { 128 | new Thread(new Runnable() { 129 | @Override 130 | public void run() { 131 | ObjectUtil objDataStream = new ObjectUtil(); 132 | objDataStream.writeObjects(object, LOCALE_CACHE_PATH); 133 | } 134 | }).start(); 135 | } 136 | 137 | 138 | 139 | /* 140 | * You can use this method to retrieve the 141 | * request response from your local cache 142 | * */ 143 | 144 | public static Object readObjectFromDisk() { 145 | ObjectUtil objDataStream = new ObjectUtil(); 146 | return objDataStream.readObjects(LOCALE_CACHE_PATH); 147 | } 148 | 149 | 150 | /* 151 | * Method to open share intent to share news 152 | * to others 153 | * */ 154 | 155 | 156 | public static void share(Activity activity, String message) { 157 | Intent shareIntent = new Intent(); 158 | shareIntent.setAction(Intent.ACTION_SEND); 159 | shareIntent.putExtra(Intent.EXTRA_TEXT, message); 160 | shareIntent.setType("text/plain"); 161 | activity.startActivity(Intent.createChooser(shareIntent, "Share news via:")); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/activity/FeedListActivity.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.activity; 2 | 3 | 4 | import android.app.SearchManager; 5 | import android.content.Context; 6 | import android.graphics.Typeface; 7 | import android.os.Bundle; 8 | import android.support.annotation.Nullable; 9 | import android.support.v7.widget.LinearLayoutManager; 10 | import android.support.v7.widget.RecyclerView; 11 | import android.support.v7.widget.SearchView; 12 | import android.view.inputmethod.EditorInfo; 13 | import android.widget.EditText; 14 | 15 | import com.an.inshorts.BaseConstants; 16 | import com.an.inshorts.R; 17 | import com.an.inshorts.adapter.NewsListAdapter; 18 | import com.an.inshorts.listener.OnViewItemClickListener; 19 | import com.an.inshorts.model.Feed; 20 | import com.an.inshorts.model.MenuItem; 21 | import com.an.inshorts.service.FeedService; 22 | import com.an.inshorts.service.FeedServiceImpl; 23 | import com.an.inshorts.utils.BaseUtils; 24 | import com.an.inshorts.views.PaginationScrollListener; 25 | 26 | import java.util.List; 27 | 28 | public class FeedListActivity extends BaseActivity implements OnViewItemClickListener, SearchView.OnQueryTextListener, BaseConstants { 29 | 30 | private SearchView searchView; 31 | 32 | private RecyclerView recyclerView; 33 | private NewsListAdapter adapter; 34 | private List feeds; 35 | 36 | private FeedService feedService; 37 | private String categoryName; 38 | 39 | // Index from which pagination should start (0 is 1st page in our case) 40 | private static final int PAGE_START = 0; 41 | // Indicates if footer ProgressBar is shown (i.e. next page is loading) 42 | private boolean isLoading = false; 43 | // If current page is the last page (Pagination will stop after this page load) 44 | private boolean isLastPage = false; 45 | // indicates the current page which Pagination is fetching. 46 | private int currentPage = PAGE_START; 47 | 48 | @Override 49 | protected void onCreate(@Nullable Bundle savedInstanceState) { 50 | super.onCreate(savedInstanceState); 51 | setContentView(R.layout.activity_feed); 52 | 53 | categoryName = getIntent().getStringExtra(INTENT_CATEGORY_NAME); 54 | initToolbar(); 55 | updateToolbarTitle(CATEGORY.get(categoryName) == null ? categoryName : CATEGORY.get(categoryName)); 56 | updateBackButton(); 57 | displaySortBtn(); 58 | 59 | feedService = new FeedServiceImpl(this); 60 | 61 | recyclerView = (RecyclerView) findViewById(R.id.feed_list); 62 | searchView = (SearchView) findViewById(R.id.search); 63 | 64 | initFeed(); 65 | initSearch(); 66 | } 67 | 68 | private void initFeed() { 69 | final LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this); 70 | recyclerView.setLayoutManager(linearLayoutManager); 71 | feeds = (List) getIntent().getSerializableExtra(INTENT_FEED); 72 | adapter = new NewsListAdapter(this, feeds, feedService, this); 73 | recyclerView.setAdapter(adapter); 74 | recyclerView.addOnScrollListener(new PaginationScrollListener(linearLayoutManager) { 75 | @Override 76 | protected void loadMoreItems() { 77 | isLoading = true; 78 | currentPage += 1; 79 | final List newData = feedService.loadMoreFeed(BaseUtils.getCurrentGroup(categoryName), categoryName, adapter.getItemCount()+1); 80 | isLoading = false; 81 | adapter.addAll(newData); 82 | } 83 | 84 | @Override 85 | public int getTotalPageCount() { 86 | return 0; 87 | } 88 | 89 | @Override 90 | public boolean isLastPage() { 91 | return isLastPage; 92 | } 93 | 94 | @Override 95 | public boolean isLoading() { 96 | return isLoading; 97 | } 98 | }); 99 | } 100 | 101 | 102 | private void initSearch() { 103 | SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); 104 | searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName())); 105 | searchView.setImeOptions(EditorInfo.IME_ACTION_SEARCH); 106 | // searchView.setIconifiedByDefault(false); 107 | 108 | EditText searchEditText = (EditText) searchView.findViewById(android.support.v7.appcompat.R.id.search_src_text); 109 | searchEditText.setTextSize(new Float(getResources().getDimension(R.dimen.font_xxxxxsmall))); 110 | searchEditText.setTextColor(getResources().getColor(R.color.toolbar_text_color)); 111 | searchEditText.setHintTextColor(getResources().getColor(R.color.feed_source_text_color)); 112 | Typeface myCustomFont = Typeface.createFromAsset(getAssets(),"fonts/tiempos_h_medium.otf"); 113 | searchEditText.setTypeface(myCustomFont); 114 | searchView.setOnQueryTextListener(this); 115 | } 116 | 117 | 118 | @Override 119 | public void onFavClick(int position, boolean checked) { 120 | feedService.handleAction(ACTION_TYPE_FAV, adapter.getItem(position), checked); 121 | } 122 | 123 | @Override 124 | public void onOfflineClick(int position, boolean checked) { 125 | feedService.handleAction(ACTION_TYPE_OFFLINE, adapter.getItem(position), checked); 126 | } 127 | 128 | @Override 129 | public void onViewClick(int position) { 130 | feedService.handleAction(ACTION_TYPE_URL, adapter.getItem(position), false); 131 | } 132 | 133 | 134 | @Override 135 | public void onMenuItemClick(MenuItem item) { 136 | List sortedFeed = feedService.sortFeed(item.getTitle(), adapter.getAllItems()); 137 | adapter.updateList(sortedFeed); 138 | } 139 | 140 | @Override 141 | public boolean onQueryTextSubmit(String query) { 142 | return false; 143 | } 144 | 145 | @Override 146 | public boolean onQueryTextChange(String newText) { 147 | adapter.getFilter().filter(newText); 148 | return true; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/service/FeedServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.service; 2 | 3 | import android.content.Context; 4 | import android.widget.Toast; 5 | 6 | import com.an.inshorts.R; 7 | import com.an.inshorts.callback.RESTListener; 8 | import com.an.inshorts.listener.OnFeedChangeListener; 9 | import com.an.inshorts.model.Feed; 10 | import com.an.inshorts.rest.RESTAPITask; 11 | import com.an.inshorts.rest.RESTExecutorService; 12 | import com.an.inshorts.utils.BaseUtils; 13 | import com.an.inshorts.utils.ConnectivityStatus; 14 | import com.an.inshorts.utils.NavigatorUtils; 15 | import com.android.volley.VolleyError; 16 | 17 | import java.util.HashMap; 18 | import java.util.List; 19 | import java.util.Map; 20 | 21 | public class FeedServiceImpl extends ResponseServiceImpl implements FeedService, RESTListener { 22 | 23 | private OnFeedChangeListener feedChangeListener; 24 | public FeedServiceImpl(Context context) { 25 | super(context); 26 | this.context = context; 27 | } 28 | 29 | public FeedServiceImpl(Context context, OnFeedChangeListener feedChangeListener) { 30 | super(context); 31 | this.feedChangeListener = feedChangeListener; 32 | } 33 | 34 | @Override 35 | public void onSuccess(Object response) { 36 | /* Store the list in local cache 37 | * get 20 items from the list & update the UI 38 | * */ 39 | List feeds = BaseUtils.loadFeedData((String) response); 40 | addFeedToDb(feeds); 41 | Map> feedsMap = filterByCategory(feeds); 42 | feedChangeListener.refreshFeed(feedsMap); 43 | } 44 | 45 | @Override 46 | public void onError(VolleyError error) { 47 | if(!ConnectivityStatus.isConnected(context)) { 48 | feedChangeListener.showError(context.getString(R.string.generic_no_internet_error)); 49 | return; 50 | } 51 | feedChangeListener.showError(context.getString(R.string.generic_http_error)); 52 | } 53 | 54 | @Override 55 | public void handleAction(String type, Feed feed, boolean checked) { 56 | if(ACTION_TYPE_FAV.equalsIgnoreCase(type)) { 57 | if(checked) { 58 | addToFavourites(feed); 59 | BaseUtils.showToast(context, context.getString(R.string.added_to_favs)); 60 | } 61 | else { 62 | removeFromFavourites(feed); 63 | BaseUtils.showToast(context, context.getString(R.string.removed_from_favs)); 64 | } 65 | 66 | } else if(ACTION_TYPE_OFFLINE.equalsIgnoreCase(type)) { 67 | if(checked) { 68 | addToOfflineFeed(feed); 69 | BaseUtils.showToast(context, context.getString(R.string.added_to_offine)); 70 | } 71 | else { 72 | removeFromOfflineFeed(feed); 73 | BaseUtils.showToast(context, context.getString(R.string.removed_from_offline)); 74 | } 75 | 76 | } else if(ACTION_TYPE_URL.equalsIgnoreCase(type)) { 77 | NavigatorUtils.openWebView(context, feed); 78 | 79 | } else if(ACTION_TYPE_GET_FEED.equalsIgnoreCase(type)) { 80 | RESTExecutorService.submit(new RESTAPITask(context, METHOD_NEWS_FEED, this)); 81 | } 82 | } 83 | 84 | @Override 85 | public Map> handleMenuItemClick(String method, List data) { 86 | if(context.getString(R.string.menu_item_1).equalsIgnoreCase(method)) { 87 | return fetchFavouriteFeeds(); 88 | 89 | } else if(context.getString(R.string.menu_item_2).equalsIgnoreCase(method)) { 90 | return fetchOfflineFeeds(); 91 | 92 | } else if(context.getString(R.string.menu_item_3).equalsIgnoreCase(method)) { 93 | return filterByCategory(null); 94 | 95 | } else { 96 | return filterFeed(method, data); 97 | } 98 | } 99 | 100 | @Override 101 | public boolean isFavourite(Long id) { 102 | return isAddedToFavourite(id); 103 | } 104 | 105 | @Override 106 | public boolean isOfflineFeed(Long id) { 107 | return isAvailableOffline(id); 108 | } 109 | 110 | @Override 111 | public List sortFeed(String type, List data) { 112 | if(context.getString(R.string.sort_item_1).equalsIgnoreCase(type)) { 113 | return sortFeedDesc(data); 114 | } else return sortFeedAsc(data); 115 | } 116 | 117 | @Override 118 | public Map> filterFeed(String type, List data) { 119 | if(context.getString(R.string.filter_item_1).equalsIgnoreCase(type)) { 120 | return filterByCategory(null); 121 | } else return filterByPublisher(null); 122 | } 123 | 124 | @Override 125 | public Map> fetchFavouriteFeeds() { 126 | List favourites = getFavouriteFeeds(); 127 | if(favourites == null || favourites.isEmpty()) { 128 | feedChangeListener.showError(context.getString(R.string.error_no_favourites)); 129 | return null; 130 | } 131 | Map> favouritesMap = new HashMap<>(); 132 | favouritesMap.put(context.getString(R.string.menu_item_1), favourites); 133 | return favouritesMap; 134 | } 135 | 136 | @Override 137 | public Map> fetchOfflineFeeds() { 138 | List offlineFeeds = getOfflineFeeds(); 139 | if(offlineFeeds == null || offlineFeeds.isEmpty()) { 140 | feedChangeListener.showError(context.getString(R.string.error_no_offlines)); 141 | return null; 142 | } 143 | Map> offlineMap = new HashMap<>(); 144 | offlineMap.put(context.getString(R.string.menu_item_2), offlineFeeds); 145 | return offlineMap; 146 | } 147 | 148 | @Override 149 | public List loadMoreFeed(String type, String name, int limit) { 150 | if(FILTER_BY_CATEGORY.equalsIgnoreCase(type)) 151 | return getFeedsByCategory(name, limit); 152 | else return getFeedsByPublisher(name, limit); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/inshorts/adapter/NewsListAdapter.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts.adapter; 2 | 3 | 4 | import android.content.Context; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.Filter; 10 | import android.widget.Filterable; 11 | import android.widget.TextView; 12 | 13 | import com.an.inshorts.R; 14 | import com.an.inshorts.listener.OnViewItemClickListener; 15 | import com.an.inshorts.model.Feed; 16 | import com.an.inshorts.service.FeedService; 17 | import com.sackcentury.shinebuttonlib.ShineButton; 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | public class NewsListAdapter extends RecyclerView.Adapter implements Filterable { 23 | 24 | private Context context; 25 | private List feedList; 26 | private List filteredList; 27 | private FeedService feedService; 28 | private OnViewItemClickListener clickListener; 29 | public NewsListAdapter(Context context, List feedList, FeedService feedService, OnViewItemClickListener clickListener) { 30 | this.context = context; 31 | this.feedList = feedList; 32 | this.filteredList = feedList; 33 | this.feedService = feedService; 34 | this.clickListener = clickListener; 35 | } 36 | 37 | @Override 38 | public CustomViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 39 | View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.feed_list_item, parent, false); 40 | return new CustomViewHolder(itemView); 41 | } 42 | 43 | @Override 44 | public void onBindViewHolder(CustomViewHolder holder, int position) { 45 | Feed feed = feedList.get(position); 46 | holder.headlineTxt.setText(feed.getTitle()); 47 | holder.sourceTxt.setText(feed.getPublisher()); 48 | 49 | if(position == 0) { 50 | holder.headlineTxt.setLineSpacing(new Float(context.getResources().getDimension(R.dimen.margin)), 1f); 51 | holder.headlineTxt.setTextSize(new Float(context.getResources().getDimension(R.dimen.font_xxxsmall))); 52 | } else { 53 | holder.headlineTxt.setLineSpacing(3f, 1.5f); 54 | holder.headlineTxt.setTextSize(new Float(context.getResources().getDimension(R.dimen.font_xxxxsmall))); 55 | } 56 | 57 | holder.favBtn.setChecked(feedService.isFavourite(feed.getId())); 58 | holder.offlineBtn.setChecked(feedService.isOfflineFeed(feed.getId())); 59 | } 60 | 61 | @Override 62 | public int getItemCount() { 63 | return feedList.size(); 64 | } 65 | 66 | public List getAllItems() { 67 | return feedList; 68 | } 69 | 70 | public Feed getItem(int position) { 71 | return feedList.get(position); 72 | } 73 | 74 | public void updateList(List data) { 75 | feedList = data; 76 | notifyDataSetChanged(); 77 | } 78 | 79 | public void add(Feed feed) { 80 | feedList.add(feed); 81 | notifyItemInserted(feedList.size() - 1); 82 | } 83 | 84 | public void addAll(List feedResult) { 85 | for (Feed feed : feedResult) { 86 | add(feed); 87 | } 88 | } 89 | 90 | 91 | @Override 92 | public Filter getFilter() { 93 | return new Filter() { 94 | @Override 95 | protected FilterResults performFiltering(CharSequence charSequence) { 96 | String charString = charSequence.toString(); 97 | 98 | if(charString.isEmpty()) { 99 | feedList = filteredList; 100 | 101 | } else { 102 | List filteredList = new ArrayList<>(); 103 | for (Feed feed : feedList) { 104 | if(feed.getTitle().toLowerCase().contains(charString) || 105 | feed.getPublisher().toLowerCase().contains(charString)) { 106 | filteredList.add(feed); 107 | } 108 | } 109 | feedList = filteredList; 110 | } 111 | 112 | FilterResults filterResults = new FilterResults(); 113 | filterResults.values = feedList; 114 | return filterResults; 115 | } 116 | 117 | @Override 118 | protected void publishResults(CharSequence charSequence, FilterResults filterResults) { 119 | feedList = (List) filterResults.values; 120 | notifyDataSetChanged(); 121 | } 122 | }; 123 | } 124 | 125 | 126 | public class CustomViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { 127 | 128 | private View rootView; 129 | 130 | private TextView headlineTxt; 131 | private TextView sourceTxt; 132 | 133 | private ShineButton favBtn; 134 | private ShineButton offlineBtn; 135 | 136 | public CustomViewHolder(View itemView) { 137 | super(itemView); 138 | rootView = itemView.findViewById(R.id.root_view); 139 | rootView.setOnClickListener(this); 140 | headlineTxt = itemView.findViewById(R.id.feed_headlines); 141 | sourceTxt = itemView.findViewById(R.id.feed_source); 142 | favBtn = itemView.findViewById(R.id.img_fav); 143 | favBtn.setOnClickListener(this); 144 | offlineBtn = itemView.findViewById(R.id.img_offline); 145 | offlineBtn.setOnClickListener(this); 146 | } 147 | 148 | @Override 149 | public void onClick(View view) { 150 | int position = getLayoutPosition(); 151 | if(view == favBtn) { 152 | clickListener.onFavClick(position, favBtn.isChecked()); 153 | 154 | } else if(view == offlineBtn) { 155 | clickListener.onOfflineClick(position, offlineBtn.isChecked()); 156 | 157 | } else { 158 | clickListener.onViewClick(position); 159 | } 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/an/inshorts/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.an.inshorts; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | import android.util.Log; 7 | 8 | import com.an.inshorts.callback.RESTListener; 9 | import com.an.inshorts.db.DbExecutorService; 10 | import com.an.inshorts.db.FeedDbTask; 11 | import com.an.inshorts.model.Feed; 12 | import com.an.inshorts.rest.RESTAPITask; 13 | import com.an.inshorts.rest.RESTExecutorService; 14 | import com.an.inshorts.service.DataService; 15 | import com.an.inshorts.service.DataServiceImpl; 16 | import com.an.inshorts.service.FeedService; 17 | import com.an.inshorts.utils.BaseUtils; 18 | import com.an.inshorts.utils.CollectionUtils; 19 | import com.android.volley.VolleyError; 20 | 21 | import org.junit.Test; 22 | import org.junit.runner.RunWith; 23 | 24 | import java.util.List; 25 | import java.util.Map; 26 | 27 | import static org.junit.Assert.*; 28 | 29 | /** 30 | * Instrumentation test, which will execute on an Android device. 31 | * 32 | * @see Testing documentation 33 | */ 34 | @RunWith(AndroidJUnit4.class) 35 | public class ExampleInstrumentedTest { 36 | @Test 37 | public void useAppContext() throws Exception { 38 | // Context of the app under test. 39 | Context appContext = InstrumentationRegistry.getTargetContext(); 40 | 41 | assertEquals("com.an.inshorts", appContext.getPackageName()); 42 | } 43 | 44 | 45 | @Test 46 | public void getNewsFeedTest() throws Exception { 47 | Context appContext = InstrumentationRegistry.getTargetContext(); 48 | 49 | RESTListener restListener = new RESTListener() { 50 | @Override 51 | public void onSuccess(Object response) { 52 | Log.d(BaseConstants.METHOD_NEWS_FEED, String.format("Success: %s", response.toString())); 53 | } 54 | 55 | @Override 56 | public void onError(VolleyError error) { 57 | Log.d(BaseConstants.METHOD_NEWS_FEED, String.format("Error: %s", error.getMessage())); 58 | } 59 | }; 60 | 61 | RESTExecutorService.submit(new RESTAPITask(appContext, BaseConstants.METHOD_NEWS_FEED, restListener)); 62 | Thread.sleep(2500); 63 | } 64 | 65 | 66 | @Test 67 | public void sortFeedByDateDescTest() { 68 | Context appContext = InstrumentationRegistry.getTargetContext(); 69 | List feedList = BaseUtils.loadDummyData(appContext); 70 | List sortedList = CollectionUtils.sortFeedDesc(feedList); 71 | for(Feed feed : sortedList) { 72 | System.out.println("-------------------"); 73 | System.out.println(feed.getId()); 74 | System.out.println(feed.getTimestamp()); 75 | } 76 | } 77 | 78 | @Test 79 | public void sortFeedByDateAscTest() { 80 | Context appContext = InstrumentationRegistry.getTargetContext(); 81 | List feedList = BaseUtils.loadDummyData(appContext); 82 | List sortedList = CollectionUtils.sortFeedAsc(feedList); 83 | for(Feed feed : sortedList) { 84 | System.out.println("-------------------"); 85 | System.out.println(feed.getId()); 86 | System.out.println(feed.getTimestamp()); 87 | } 88 | } 89 | 90 | @Test 91 | public void filterByCategoryTest() { 92 | Context appContext = InstrumentationRegistry.getTargetContext(); 93 | List feedList = BaseUtils.loadDummyData(appContext); 94 | 95 | DataService dataService = new DataServiceImpl(appContext); 96 | Map> filteredData = dataService.filterByCategory(feedList); 97 | for(Map.Entry> s : filteredData.entrySet()) { 98 | System.out.println("-------------------"); 99 | System.out.println(s.getKey()); 100 | System.out.println(s.getValue().size()); 101 | } 102 | } 103 | 104 | @Test 105 | public void filterByPublisherTest() { 106 | Context appContext = InstrumentationRegistry.getTargetContext(); 107 | List feedList = BaseUtils.loadDummyData(appContext); 108 | 109 | DataService dataService = new DataServiceImpl(appContext); 110 | Map> filteredData = dataService.filterByPublisher(feedList); 111 | for(Map.Entry> s : filteredData.entrySet()) { 112 | System.out.println("-------------------"); 113 | System.out.println(s.getKey()); 114 | System.out.println(s.getValue().size()); 115 | } 116 | } 117 | 118 | 119 | @Test 120 | public void localCacheFavouritesTest() throws InterruptedException { 121 | Context appContext = InstrumentationRegistry.getTargetContext(); 122 | List feedList = BaseUtils.loadDummyData(appContext); 123 | 124 | Feed favouriteFeed1 = feedList.get(1); 125 | DbExecutorService.submit(new FeedDbTask(appContext, BaseConstants.TYPE_ADD_FAVOURITES, favouriteFeed1)); 126 | Thread.sleep(1000); 127 | 128 | DataService dataService = new DataServiceImpl(appContext); 129 | List favourites = dataService.getFavouriteFeeds(); 130 | System.out.println("Adding Favourites I: " + favourites.size()); 131 | 132 | Feed favouriteFeed2 = feedList.get(3); 133 | DbExecutorService.submit(new FeedDbTask(appContext, BaseConstants.TYPE_ADD_FAVOURITES, favouriteFeed2)); 134 | Thread.sleep(1000); 135 | 136 | favourites = dataService.getFavouriteFeeds(); 137 | System.out.println("Adding Favourites II: " + favourites.size()); 138 | 139 | DbExecutorService.submit(new FeedDbTask(appContext, BaseConstants.TYPE_REMOVE_FAVOURITES, favouriteFeed2)); 140 | Thread.sleep(1000); 141 | 142 | favourites = dataService.getFavouriteFeeds(); 143 | System.out.println("Removing Favourites II: " + favourites.size()); 144 | } 145 | 146 | 147 | @Test 148 | public void localCacheOfflineFeedsTest() throws InterruptedException { 149 | Context appContext = InstrumentationRegistry.getTargetContext(); 150 | List feedList = BaseUtils.loadDummyData(appContext); 151 | 152 | Feed favouriteFeed1 = feedList.get(10); 153 | DbExecutorService.submit(new FeedDbTask(appContext, BaseConstants.TYPE_ADD_OFFLINE, favouriteFeed1)); 154 | Thread.sleep(1000); 155 | 156 | DataService dataService = new DataServiceImpl(appContext); 157 | List offlineFeeds = dataService.getOfflineFeeds(); 158 | System.out.println("Adding Offline I: " + offlineFeeds.size()); 159 | 160 | Feed favouriteFeed2 = feedList.get(20); 161 | DbExecutorService.submit(new FeedDbTask(appContext, BaseConstants.TYPE_ADD_OFFLINE, favouriteFeed2)); 162 | Thread.sleep(1000); 163 | 164 | offlineFeeds = dataService.getOfflineFeeds(); 165 | System.out.println("Adding Offline II: " + offlineFeeds.size()); 166 | 167 | DbExecutorService.submit(new FeedDbTask(appContext, BaseConstants.TYPE_REMOVE_OFFLINE, favouriteFeed2)); 168 | Thread.sleep(1000); 169 | 170 | offlineFeeds = dataService.getOfflineFeeds(); 171 | System.out.println("Removing Offline II: " + offlineFeeds.size()); 172 | } 173 | } 174 | --------------------------------------------------------------------------------