├── .gitignore ├── .travis.yml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── gradle.properties ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── it │ │ └── cosenonjaviste │ │ ├── androidtest │ │ ├── base │ │ │ ├── FragmentRule.java │ │ │ ├── MockWebServerWrapper.java │ │ │ └── OrientationChangeAction.java │ │ └── utils │ │ │ └── TestUtils.java │ │ └── ui │ │ ├── CnjDaggerRule.java │ │ ├── MainActivityTest.java │ │ ├── author │ │ └── AuthorListFragmentTest.java │ │ ├── category │ │ └── CategoryListFragmentTest.java │ │ ├── contact │ │ └── ContactFragmentTest.java │ │ ├── page │ │ └── PageFragmentTest.java │ │ ├── post │ │ └── PostListFragmentTest.java │ │ └── twitter │ │ └── TweetListFragmentTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── style.css │ ├── java │ │ └── it │ │ │ └── cosenonjaviste │ │ │ ├── core │ │ │ ├── Navigator.java │ │ │ ├── author │ │ │ │ ├── AuthorListModel.java │ │ │ │ └── AuthorListViewModel.java │ │ │ ├── base │ │ │ │ ├── ArgumentManager.java │ │ │ │ ├── RxViewModel.java │ │ │ │ └── ViewModel.java │ │ │ ├── category │ │ │ │ ├── CategoryListModel.java │ │ │ │ └── CategoryListViewModel.java │ │ │ ├── contact │ │ │ │ ├── ContactModel.java │ │ │ │ └── ContactViewModel.java │ │ │ ├── list │ │ │ │ ├── GenericRxListViewModel.java │ │ │ │ ├── ListModel.java │ │ │ │ └── RxListViewModel.java │ │ │ ├── page │ │ │ │ ├── PageModel.java │ │ │ │ └── PageViewModel.java │ │ │ ├── post │ │ │ │ ├── PostListArgument.java │ │ │ │ ├── PostListModel.java │ │ │ │ └── PostListViewModel.java │ │ │ ├── twitter │ │ │ │ ├── TweetListModel.java │ │ │ │ └── TweetListViewModel.java │ │ │ └── utils │ │ │ │ ├── DenvelopingConverter.java │ │ │ │ ├── EmailVerifier.java │ │ │ │ ├── EnvelopePayload.java │ │ │ │ ├── Md5Utils.java │ │ │ │ └── ObservableArrayListBagger.java │ │ │ ├── model │ │ │ ├── Attachment.java │ │ │ ├── Author.java │ │ │ ├── Category.java │ │ │ ├── MailJetService.java │ │ │ ├── MyAdapterFactory.java │ │ │ ├── Post.java │ │ │ ├── Tweet.java │ │ │ ├── TwitterService.java │ │ │ └── WordPressService.java │ │ │ └── ui │ │ │ ├── AndroidNavigator.java │ │ │ ├── AppModule.java │ │ │ ├── ApplicationComponent.java │ │ │ ├── CoseNonJavisteApp.java │ │ │ ├── MainActivity.java │ │ │ ├── MessageManager.java │ │ │ ├── author │ │ │ ├── AuthorListFragment.java │ │ │ └── AuthorViewHolder.java │ │ │ ├── bind │ │ │ └── DataBindingConverters.java │ │ │ ├── category │ │ │ ├── CategoryListFragment.java │ │ │ └── CategoryViewHolder.java │ │ │ ├── contact │ │ │ └── ContactFragment.java │ │ │ ├── page │ │ │ └── PageFragment.java │ │ │ ├── post │ │ │ ├── PostListFragment.java │ │ │ └── PostViewHolder.java │ │ │ ├── recycler │ │ │ ├── AdapterOnListChangedCallback.java │ │ │ ├── BindableAdapter.java │ │ │ ├── BindableViewHolder.java │ │ │ └── WeakOnListChangedCallback.java │ │ │ ├── twitter │ │ │ ├── TweetListFragment.java │ │ │ └── TweetViewHolder.java │ │ │ └── utils │ │ │ ├── CircleTransform.java │ │ │ ├── DateFormatter.java │ │ │ ├── EndlessRecyclerOnScrollListener.java │ │ │ ├── RecyclerBindingBuilder.java │ │ │ ├── SingleFragmentActivity.java │ │ │ └── TextWatcherAdapter.java │ ├── project.properties │ └── res │ │ ├── drawable-hdpi │ │ ├── drawer_shadow.9.png │ │ ├── ic_email_black_24dp.png │ │ ├── ic_home_black_24dp.png │ │ ├── ic_label_outline_black_24dp.png │ │ ├── ic_menu.png │ │ ├── ic_person_black_24dp.png │ │ ├── ic_public_black_24dp.png │ │ ├── ic_share_white_24dp.png │ │ ├── progress_in.png │ │ └── progress_out.png │ │ ├── drawable-mdpi │ │ ├── drawer_shadow.9.png │ │ ├── ic_email_black_24dp.png │ │ ├── ic_home_black_24dp.png │ │ ├── ic_label_outline_black_24dp.png │ │ ├── ic_menu.png │ │ ├── ic_person_black_24dp.png │ │ ├── ic_public_black_24dp.png │ │ ├── ic_share_white_24dp.png │ │ ├── progress_in.png │ │ └── progress_out.png │ │ ├── drawable-xhdpi │ │ ├── drawer_shadow.9.png │ │ ├── ic_email_black_24dp.png │ │ ├── ic_home_black_24dp.png │ │ ├── ic_label_outline_black_24dp.png │ │ ├── ic_menu.png │ │ ├── ic_person_black_24dp.png │ │ ├── ic_public_black_24dp.png │ │ ├── ic_share_white_24dp.png │ │ ├── progress_in.png │ │ └── progress_out.png │ │ ├── drawable-xxhdpi │ │ ├── ic_email_black_24dp.png │ │ ├── ic_home_black_24dp.png │ │ ├── ic_label_outline_black_24dp.png │ │ ├── ic_menu.png │ │ ├── ic_person_black_24dp.png │ │ ├── ic_public_black_24dp.png │ │ ├── ic_share_white_24dp.png │ │ ├── progress_in.png │ │ └── progress_out.png │ │ ├── drawable-xxxhdpi │ │ ├── ic_email_black_24dp.png │ │ ├── ic_home_black_24dp.png │ │ ├── ic_label_outline_black_24dp.png │ │ ├── ic_menu.png │ │ ├── ic_person_black_24dp.png │ │ ├── ic_public_black_24dp.png │ │ └── ic_share_white_24dp.png │ │ ├── drawable │ │ ├── detail_shadow.xml │ │ ├── drawer_background.xml │ │ ├── logocnj.png │ │ └── progress_bar.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── author_cell.xml │ │ ├── category_row.xml │ │ ├── contact.xml │ │ ├── drawer_header.xml │ │ ├── load_more_error_view.xml │ │ ├── post_detail.xml │ │ ├── post_row.xml │ │ ├── recycler.xml │ │ ├── single_fragment.xml │ │ └── tweet_row.xml │ │ ├── menu │ │ └── drawer.xml │ │ ├── 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 │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ ├── sharedTest │ └── java │ │ └── it │ │ └── cosenonjaviste │ │ └── TestData.java │ └── test │ └── java │ ├── it │ └── cosenonjaviste │ │ └── core │ │ ├── CnjJUnitDaggerRule.java │ │ ├── ParcelableTester.java │ │ ├── author │ │ └── AuthorListViewModelTest.java │ │ ├── category │ │ └── CategoryListViewModelTest.java │ │ ├── contact │ │ └── ContactViewModelTest.java │ │ ├── model │ │ ├── JsonStubs.java │ │ └── WordPressServiceTest.java │ │ ├── page │ │ └── PageViewModelTest.java │ │ ├── post │ │ ├── AuthorPostListViewModelTest.java │ │ ├── CategoryPostListViewModelTest.java │ │ └── PostListViewModelTest.java │ │ └── twitter │ │ └── TweetListViewModelTest.java │ └── org │ └── mockito │ └── configuration │ └── MockitoConfiguration.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── install-dependencies.sh ├── project.properties ├── settings.gradle └── wait.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/* 4 | .DS_Store 5 | /build 6 | *.iml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | jdk: 3 | - oraclejdk8 4 | 5 | sudo: false 6 | 7 | cache: 8 | directories: 9 | - $HOME/.gradle/caches/ 10 | 11 | env: 12 | global: 13 | MALLOC_ARENA_MAX=2 14 | matrix: 15 | - ANDROID_TARGET=android-18 ANDROID_ABI=armeabi-v7a 16 | 17 | android: 18 | components: 19 | - tools 20 | - platform-tools 21 | - build-tools-25.0.2 22 | - android-25 23 | - extra-google-m2repository 24 | - extra-android-m2repository 25 | - sys-img-armeabi-v7a-android-18 26 | 27 | before_script: 28 | - export "JAVA_OPTS=-Xmx1024m" 29 | - export "JAVA7_HOME=/usr/lib/jvm/java-7-oracle" 30 | - export "JAVA8_HOME=/usr/lib/jvm/java-8-oracle" 31 | # - echo no | android create avd --force -n test -t $ANDROID_TARGET --abi $ANDROID_ABI --skin WVGA800 32 | # - ./gradlew :app:assembleDebug 33 | # - emulator -avd test -no-skin -no-audio -no-window & 34 | # - android-wait-for-emulator 35 | # - adb shell input keyevent 82 & 36 | 37 | script: 38 | - ./gradlew :app:test :app:jacocoUnitTestReport :app:coveralls --stacktrace 39 | # - ./gradlew :app:test :app:connectedAndroidTest :app:jacocoTestReport :app:jacocoUnitTestReport :app:coveralls --stacktrace -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodingJamAndroidApp 2 | Android app of blog [codingjam.it](http://codingjam.it) 3 | 4 | [![Build Status](https://travis-ci.org/coding-jam/CodingJamAndroidApp.svg?branch=master)](https://travis-ci.org/coding-jam/CodingJamAndroidApp) 5 | [![Coverage Status](https://coveralls.io/repos/github/coding-jam/CodingJamAndroidApp/badge.svg?branch=master)](https://coveralls.io/github/coding-jam/CodingJamAndroidApp?branch=master) 6 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/gradle.properties: -------------------------------------------------------------------------------- 1 | oauth_consumerKey="123" 2 | oauth_consumerSecret="456" 3 | oauth_accessToken="789" 4 | oauth_accessTokenSecret="012" 5 | mailjet_userName="" 6 | mailJetPassword="" 7 | -------------------------------------------------------------------------------- /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 /Applications/Android Studio.app/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/androidTest/java/it/cosenonjaviste/androidtest/base/FragmentRule.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.androidtest.base; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.os.Parcelable; 7 | import android.support.test.InstrumentationRegistry; 8 | import android.support.test.rule.ActivityTestRule; 9 | import android.support.v4.app.Fragment; 10 | 11 | import it.cosenonjaviste.core.base.ViewModel; 12 | import it.cosenonjaviste.ui.utils.SingleFragmentActivity; 13 | 14 | public class FragmentRule extends ActivityTestRule { 15 | private Class fragmentClass; 16 | 17 | public FragmentRule(Class fragmentClass) { 18 | super(SingleFragmentActivity.class, false, false); 19 | this.fragmentClass = fragmentClass; 20 | } 21 | 22 | public SingleFragmentActivity launchFragment() { 23 | return launchFragment(null); 24 | } 25 | 26 | public void launchFragment(Parcelable model) { 27 | Bundle bundle = new Bundle(); 28 | bundle.putParcelable(ViewModel.MODEL, model); 29 | launchFragment(bundle); 30 | } 31 | 32 | public SingleFragmentActivity launchFragment(Bundle b) { 33 | Intent intent = SingleFragmentActivity.populateIntent(new Intent(), fragmentClass); 34 | if (b != null) { 35 | intent.putExtras(b); 36 | } 37 | return launchActivity(intent); 38 | } 39 | 40 | public T getApplication() { 41 | return (T) InstrumentationRegistry.getTargetContext().getApplicationContext(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/androidTest/java/it/cosenonjaviste/androidtest/base/MockWebServerWrapper.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.androidtest.base; 2 | 3 | 4 | import java.io.IOException; 5 | import java.util.LinkedList; 6 | import java.util.concurrent.ExecutorService; 7 | import java.util.concurrent.Executors; 8 | import java.util.function.Function; 9 | 10 | import okhttp3.mockwebserver.Dispatcher; 11 | import okhttp3.mockwebserver.MockResponse; 12 | import okhttp3.mockwebserver.MockWebServer; 13 | import okhttp3.mockwebserver.RecordedRequest; 14 | 15 | public class MockWebServerWrapper { 16 | 17 | private static MockWebServer server; 18 | 19 | private static LinkedList requests = new LinkedList<>(); 20 | 21 | private static Function dispatchFunction; 22 | 23 | public MockWebServerWrapper() { 24 | if (server == null) { 25 | server = new MockWebServer(); 26 | try { 27 | server.start(); 28 | } catch (IOException e) { 29 | throw new RuntimeException(e); 30 | } 31 | initDispatcher(); 32 | } 33 | } 34 | 35 | public static void initDispatcher(Function dispatchFunction) { 36 | MockWebServerWrapper.dispatchFunction = dispatchFunction; 37 | } 38 | 39 | public void initDispatcher(String responseBody) { 40 | dispatchFunction = recordedRequest -> new MockResponse().setBody(responseBody); 41 | requests = new LinkedList<>(); 42 | } 43 | 44 | private void initDispatcher() { 45 | server.setDispatcher(new Dispatcher() { 46 | @Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException { 47 | requests.add(request); 48 | return dispatchFunction.apply(request); 49 | } 50 | }); 51 | } 52 | 53 | public String getUrl(boolean initInBackgroundThread) { 54 | if (initInBackgroundThread) { 55 | try { 56 | ExecutorService executorService = Executors.newSingleThreadExecutor(); 57 | return executorService.submit(this::getUrlSync).get(); 58 | } catch (Throwable e) { 59 | throw new RuntimeException(e); 60 | } 61 | } else { 62 | return getUrlSync(); 63 | } 64 | } 65 | 66 | private String getUrlSync() { 67 | return server.url("/").toString(); 68 | } 69 | 70 | public void shutdown() { 71 | try { 72 | server.shutdown(); 73 | } catch (IOException e) { 74 | throw new RuntimeException(e); 75 | } 76 | } 77 | 78 | public int getRequestCount() { 79 | return requests.size(); 80 | } 81 | 82 | public String getLastUrl() { 83 | return requests.getLast().getPath(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/src/androidTest/java/it/cosenonjaviste/androidtest/base/OrientationChangeAction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2015 - Nathan Barraille 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | * 24 | */ 25 | package it.cosenonjaviste.androidtest.base; 26 | 27 | import android.app.Activity; 28 | import android.content.pm.ActivityInfo; 29 | import android.support.test.espresso.UiController; 30 | import android.support.test.espresso.ViewAction; 31 | import android.support.test.runner.lifecycle.ActivityLifecycleMonitorRegistry; 32 | import android.support.test.runner.lifecycle.Stage; 33 | import android.view.View; 34 | 35 | import org.hamcrest.Matcher; 36 | 37 | import java.util.Collection; 38 | 39 | import static android.support.test.espresso.matcher.ViewMatchers.isRoot; 40 | 41 | /** 42 | * An Espresso ViewAction that changes the orientation of the screen 43 | */ 44 | public class OrientationChangeAction implements ViewAction { 45 | private final int orientation; 46 | 47 | private OrientationChangeAction(int orientation) { 48 | this.orientation = orientation; 49 | } 50 | 51 | @Override 52 | public Matcher getConstraints() { 53 | return isRoot(); 54 | } 55 | 56 | @Override 57 | public String getDescription() { 58 | return "change orientation to " + orientation; 59 | } 60 | 61 | @Override 62 | public void perform(UiController uiController, View view) { 63 | uiController.loopMainThreadUntilIdle(); 64 | final Activity activity = (Activity) view.getContext(); 65 | activity.setRequestedOrientation(orientation); 66 | 67 | Collection resumedActivities = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED); 68 | if (resumedActivities.isEmpty()) { 69 | throw new RuntimeException("Could not change orientation"); 70 | } 71 | } 72 | 73 | public static ViewAction orientationLandscape() { 74 | return new OrientationChangeAction(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); 75 | } 76 | 77 | public static ViewAction orientationPortrait() { 78 | return new OrientationChangeAction(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 79 | } 80 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/it/cosenonjaviste/androidtest/utils/TestUtils.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.androidtest.utils; 2 | 3 | 4 | import com.annimon.stream.function.Consumer; 5 | 6 | public class TestUtils { 7 | 8 | public static Consumer sleepAction() { 9 | return o -> sleep(1); 10 | } 11 | 12 | public static void sleep(int seconds) { 13 | // try { 14 | // Thread.sleep(seconds * 1000); 15 | // } catch (InterruptedException ignored) { 16 | // } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/androidTest/java/it/cosenonjaviste/ui/CnjDaggerRule.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui; 2 | 3 | import android.os.AsyncTask; 4 | import android.support.test.InstrumentationRegistry; 5 | 6 | import org.junit.runners.model.FrameworkMethod; 7 | import org.junit.runners.model.Statement; 8 | 9 | import io.reactivex.Scheduler; 10 | import io.reactivex.plugins.RxJavaPlugins; 11 | import io.reactivex.schedulers.Schedulers; 12 | import it.cosenonjaviste.daggermock.DaggerMockRule; 13 | import it.cosenonjaviste.model.TwitterService; 14 | import it.cosenonjaviste.model.WordPressService; 15 | 16 | public class CnjDaggerRule extends DaggerMockRule { 17 | public CnjDaggerRule() { 18 | super(ApplicationComponent.class, new AppModule(getApp())); 19 | providesMock(WordPressService.class, TwitterService.class); 20 | set(component -> getApp().setComponent(component)); 21 | } 22 | 23 | public static CoseNonJavisteApp getApp() { 24 | return (CoseNonJavisteApp) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext(); 25 | } 26 | 27 | @Override public Statement apply(Statement base, FrameworkMethod method, Object target) { 28 | Statement superStatement = super.apply(base, method, target); 29 | Scheduler asyncTaskScheduler = 30 | Schedulers.from(AsyncTask.THREAD_POOL_EXECUTOR); 31 | return new Statement() { 32 | @Override public void evaluate() throws Throwable { 33 | RxJavaPlugins.setIoSchedulerHandler( 34 | scheduler -> asyncTaskScheduler); 35 | RxJavaPlugins.setComputationSchedulerHandler( 36 | scheduler -> asyncTaskScheduler); 37 | RxJavaPlugins.setNewThreadSchedulerHandler( 38 | scheduler -> asyncTaskScheduler); 39 | 40 | try { 41 | superStatement.evaluate(); 42 | } finally { 43 | RxJavaPlugins.reset(); 44 | } 45 | } 46 | }; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/androidTest/java/it/cosenonjaviste/ui/MainActivityTest.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui; 2 | 3 | //@RunWith(AndroidJUnit4.class) 4 | public class MainActivityTest { 5 | // @Inject WordPressService wordPressService; 6 | // 7 | // @Inject MockWebServerWrapper server; 8 | // 9 | // @Inject TwitterService twitterService; 10 | // 11 | // @Rule public ActivityTestRule activityRule = new ActivityTestRule<>(MainActivity.class, false, false); 12 | // 13 | // @Before public void setUp() { 14 | // DaggerUtils.createTestComponent().inject(this); 15 | // 16 | // when(wordPressService.listPosts(eq(1))) 17 | // .thenReturn(TestData.postResponse(10)); 18 | // when(wordPressService.listCategories()) 19 | // .thenReturn(TestData.categoryResponse(3)); 20 | // when(wordPressService.listAuthors()) 21 | // .thenReturn(TestData.authorResponse(2)); 22 | // when(wordPressService.listAuthorPosts(anyLong(), anyInt())) 23 | // .thenReturn(TestData.postResponse(1)); 24 | // 25 | // when(twitterService.loadTweets(eq(1))) 26 | // .thenReturn(TestData.tweets()); 27 | // 28 | // server.initDispatcher("CoseNonJaviste"); 29 | // } 30 | // 31 | // @Test public void showMainActivity() { 32 | // activityRule.launchActivity(null); 33 | // } 34 | // 35 | // @Test public void showCategories() { 36 | // activityRule.launchActivity(null); 37 | // clickOnDrawer(R.string.categories); 38 | //// onView(withText("cat 0")).check(matches(isDisplayed())); 39 | // } 40 | // 41 | // @Test public void showAuthors() { 42 | // activityRule.launchActivity(null); 43 | // clickOnDrawer(R.string.authors); 44 | //// onView(withText("name 0")).check(matches(isDisplayed())); 45 | // } 46 | // 47 | // @Test public void showTweets() { 48 | // activityRule.launchActivity(null); 49 | // clickOnDrawer(R.string.twitter); 50 | //// onView(withText("tweet text 1")).check(matches(isDisplayed())); 51 | // } 52 | // 53 | // @Test public void showContactForm() { 54 | // activityRule.launchActivity(null); 55 | // clickOnDrawer(R.string.contacts); 56 | // } 57 | // 58 | // private void clickOnDrawer(int text) { 59 | // onView(withClassName(endsWith("ImageButton"))).perform(click()); 60 | // 61 | //// onView(withText(text)).perform(click()); 62 | // } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/androidTest/java/it/cosenonjaviste/ui/author/AuthorListFragmentTest.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui.author; 2 | 3 | import org.junit.Rule; 4 | import org.junit.Test; 5 | import org.mockito.Mock; 6 | 7 | import it.cosenonjaviste.TestData; 8 | import it.cosenonjaviste.androidtest.base.FragmentRule; 9 | import it.cosenonjaviste.core.author.AuthorListModel; 10 | import it.cosenonjaviste.model.WordPressService; 11 | import it.cosenonjaviste.ui.CnjDaggerRule; 12 | 13 | import static android.support.test.espresso.Espresso.onView; 14 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 15 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 16 | import static android.support.test.espresso.matcher.ViewMatchers.withText; 17 | import static org.mockito.Mockito.when; 18 | 19 | public class AuthorListFragmentTest { 20 | 21 | @Mock WordPressService wordPressService; 22 | 23 | @Rule public FragmentRule fragmentRule = new FragmentRule(AuthorListFragment.class); 24 | 25 | @Rule public final CnjDaggerRule daggerRule = new CnjDaggerRule(); 26 | 27 | @Test 28 | public void testAuthorList() { 29 | when(wordPressService.listAuthors()) 30 | .thenReturn(TestData.authorResponse(8)); 31 | 32 | fragmentRule.launchFragment(new AuthorListModel()); 33 | 34 | onView(withText("name 1")).check(matches(isDisplayed())); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/androidTest/java/it/cosenonjaviste/ui/category/CategoryListFragmentTest.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui.category; 2 | 3 | import org.junit.Rule; 4 | import org.junit.Test; 5 | import org.mockito.Mock; 6 | 7 | import java.io.IOException; 8 | 9 | import io.reactivex.Single; 10 | import it.cosenonjaviste.TestData; 11 | import it.cosenonjaviste.androidtest.base.FragmentRule; 12 | import it.cosenonjaviste.core.category.CategoryListModel; 13 | import it.cosenonjaviste.model.WordPressService; 14 | import it.cosenonjaviste.ui.CnjDaggerRule; 15 | 16 | import static android.support.test.espresso.Espresso.onView; 17 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 18 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 19 | import static android.support.test.espresso.matcher.ViewMatchers.withText; 20 | import static org.mockito.Mockito.verify; 21 | import static org.mockito.Mockito.when; 22 | 23 | public class CategoryListFragmentTest { 24 | 25 | @Mock WordPressService wordPressService; 26 | 27 | @Rule public FragmentRule fragmentRule = new FragmentRule(CategoryListFragment.class); 28 | 29 | @Rule public final CnjDaggerRule daggerRule = new CnjDaggerRule(); 30 | 31 | @Test public void testCategoryList() { 32 | when(wordPressService.listCategories()) 33 | .thenReturn(TestData.categoryResponse(3)); 34 | 35 | fragmentRule.launchFragment(new CategoryListModel()); 36 | 37 | onView(withText("cat 1")).check(matches(isDisplayed())); 38 | } 39 | 40 | @Test public void testCategoryError() { 41 | when(wordPressService.listCategories()) 42 | .thenReturn(Single.error(new IOException("bla bla bla"))); 43 | 44 | fragmentRule.launchFragment(new CategoryListModel()); 45 | 46 | verify(wordPressService).listCategories(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/androidTest/java/it/cosenonjaviste/ui/contact/ContactFragmentTest.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui.contact; 2 | 3 | import android.support.test.espresso.action.ViewActions; 4 | 5 | import org.junit.Before; 6 | import org.junit.Rule; 7 | import org.junit.Test; 8 | import org.mockito.Mock; 9 | 10 | import io.reactivex.Completable; 11 | import it.cosenonjaviste.R; 12 | import it.cosenonjaviste.androidtest.base.FragmentRule; 13 | import it.cosenonjaviste.model.MailJetService; 14 | import it.cosenonjaviste.ui.CnjDaggerRule; 15 | 16 | import static android.support.test.espresso.Espresso.onView; 17 | import static android.support.test.espresso.action.ViewActions.click; 18 | import static android.support.test.espresso.action.ViewActions.scrollTo; 19 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 20 | import static org.mockito.Matchers.anyString; 21 | import static org.mockito.Mockito.when; 22 | 23 | public class ContactFragmentTest { 24 | @Rule public FragmentRule fragmentRule = new FragmentRule(ContactFragment.class); 25 | 26 | @Mock MailJetService mailJetService; 27 | 28 | @Rule public final CnjDaggerRule daggerRule = new CnjDaggerRule(); 29 | 30 | @Before public void setUp() { 31 | when(mailJetService.sendEmail(anyString(), anyString(), anyString(), anyString())) 32 | .thenReturn(Completable.complete()); 33 | } 34 | 35 | @Test public void testContactFragment() { 36 | fragmentRule.launchFragment(); 37 | 38 | onView(withId(R.id.name)).perform(ViewActions.typeText("name")); 39 | onView(withId(R.id.email)).perform(ViewActions.typeText("email@email.it")); 40 | onView(withId(R.id.message)).perform(ViewActions.typeText("message")); 41 | 42 | onView(withId(R.id.send_button)).perform(scrollTo(), click()); 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/it/cosenonjaviste/ui/page/PageFragmentTest.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui.page; 2 | 3 | import org.junit.Before; 4 | import org.junit.Rule; 5 | import org.junit.Test; 6 | 7 | import it.cosenonjaviste.TestData; 8 | import it.cosenonjaviste.androidtest.base.FragmentRule; 9 | import it.cosenonjaviste.androidtest.base.MockWebServerWrapper; 10 | import it.cosenonjaviste.androidtest.utils.TestUtils; 11 | import it.cosenonjaviste.ui.CnjDaggerRule; 12 | 13 | public class PageFragmentTest { 14 | 15 | MockWebServerWrapper server = new MockWebServerWrapper(); 16 | 17 | @Rule public FragmentRule fragmentRule = new FragmentRule(PageFragment.class); 18 | 19 | @Rule public final CnjDaggerRule daggerRule = new CnjDaggerRule(); 20 | 21 | @Before public void setUp() { 22 | server.initDispatcher("CoseNonJaviste
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A"); 23 | } 24 | 25 | @Test public void testDetailFragment() { 26 | fragmentRule.launchFragment(TestData.createPost(1, server.getUrl(false) + "abc")); 27 | TestUtils.sleep(100); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/androidTest/java/it/cosenonjaviste/ui/post/PostListFragmentTest.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui.post; 2 | 3 | import android.support.test.espresso.contrib.RecyclerViewActions; 4 | 5 | import org.junit.Before; 6 | import org.junit.Rule; 7 | import org.junit.Test; 8 | import org.mockito.Mock; 9 | 10 | import it.cosenonjaviste.R; 11 | import it.cosenonjaviste.TestData; 12 | import it.cosenonjaviste.androidtest.base.FragmentRule; 13 | import it.cosenonjaviste.core.post.PostListModel; 14 | import it.cosenonjaviste.model.WordPressService; 15 | import it.cosenonjaviste.ui.CnjDaggerRule; 16 | 17 | import static android.support.test.espresso.Espresso.onView; 18 | import static android.support.test.espresso.action.ViewActions.click; 19 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 20 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 21 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 22 | import static android.support.test.espresso.matcher.ViewMatchers.withText; 23 | import static org.mockito.Matchers.eq; 24 | import static org.mockito.Mockito.when; 25 | 26 | public class PostListFragmentTest { 27 | 28 | @Mock WordPressService wordPressService; 29 | 30 | @Rule public FragmentRule fragmentRule = new FragmentRule(PostListFragment.class); 31 | 32 | @Rule public final CnjDaggerRule daggerRule = new CnjDaggerRule(); 33 | 34 | @Before public void setUp() { 35 | when(wordPressService.listPosts(eq(1))) 36 | .thenReturn(TestData.postResponse(0, 10)); 37 | when(wordPressService.listPosts(eq(2))) 38 | .thenReturn(TestData.postResponse(10, 10)); 39 | } 40 | 41 | @Test public void testPostList() throws InterruptedException { 42 | fragmentRule.launchFragment(new PostListModel()); 43 | 44 | onView(withText("post title 1")).check(matches(isDisplayed())); 45 | } 46 | 47 | @Test public void testGoToPostDetail() { 48 | fragmentRule.launchFragment(new PostListModel()); 49 | 50 | onView(withId(R.id.list)) 51 | .perform(RecyclerViewActions.actionOnItemAtPosition(3, click())); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/androidTest/java/it/cosenonjaviste/ui/twitter/TweetListFragmentTest.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui.twitter; 2 | 3 | import android.support.test.runner.AndroidJUnit4; 4 | 5 | import org.junit.Rule; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.mockito.Mock; 9 | 10 | import it.cosenonjaviste.TestData; 11 | import it.cosenonjaviste.androidtest.base.FragmentRule; 12 | import it.cosenonjaviste.core.twitter.TweetListModel; 13 | import it.cosenonjaviste.model.TwitterService; 14 | import it.cosenonjaviste.ui.CnjDaggerRule; 15 | 16 | import static android.support.test.espresso.Espresso.onView; 17 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 18 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 19 | import static android.support.test.espresso.matcher.ViewMatchers.withText; 20 | import static org.mockito.Matchers.eq; 21 | import static org.mockito.Mockito.when; 22 | 23 | @RunWith(AndroidJUnit4.class) 24 | public class TweetListFragmentTest { 25 | 26 | @Mock TwitterService twitterService; 27 | 28 | @Rule public FragmentRule fragmentRule = new FragmentRule(TweetListFragment.class); 29 | 30 | @Rule public final CnjDaggerRule daggerRule = new CnjDaggerRule(); 31 | 32 | @Test public void testTweetList() { 33 | when(twitterService.loadTweets(eq(1))) 34 | .thenReturn(TestData.tweets(10)); 35 | 36 | fragmentRule.launchFragment(new TweetListModel()); 37 | 38 | onView(withText("tweet text 1")).check(matches(isDisplayed())); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/Navigator.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.core; 2 | 3 | import it.codingjam.lifecyclebinder.DefaultLifeCycleAware; 4 | import it.cosenonjaviste.core.post.PostListArgument; 5 | import it.cosenonjaviste.model.Post; 6 | 7 | public abstract class Navigator extends DefaultLifeCycleAware { 8 | public abstract void openPostList(PostListArgument argument); 9 | 10 | public abstract void openDetail(Post post); 11 | 12 | public abstract void share(String subject, String text); 13 | 14 | public abstract void showMessage(int message); 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/author/AuthorListModel.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.core.author; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import com.hannesdorfmann.parcelableplease.annotation.ParcelablePlease; 7 | 8 | import it.cosenonjaviste.core.list.ListModel; 9 | import it.cosenonjaviste.model.Author; 10 | 11 | @ParcelablePlease 12 | public class AuthorListModel extends ListModel implements Parcelable { 13 | 14 | public int size() { 15 | return items.size(); 16 | } 17 | 18 | public Author get(int index) { 19 | return items.get(index); 20 | } 21 | 22 | @Override public int describeContents() { 23 | return 0; 24 | } 25 | 26 | @Override public void writeToParcel(Parcel dest, int flags) { 27 | AuthorListModelParcelablePlease.writeToParcel(this, dest, flags); 28 | } 29 | 30 | public static final Creator CREATOR = new Creator() { 31 | public AuthorListModel createFromParcel(Parcel source) { 32 | AuthorListModel target = new AuthorListModel(); 33 | AuthorListModelParcelablePlease.readFromParcel(target, source); 34 | return target; 35 | } 36 | 37 | public AuthorListModel[] newArray(int size) { 38 | return new AuthorListModel[size]; 39 | } 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/author/AuthorListViewModel.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.core.author; 2 | 3 | import android.databinding.ObservableBoolean; 4 | import android.support.annotation.NonNull; 5 | 6 | import com.nytimes.android.external.store2.base.impl.Store; 7 | 8 | import java.util.Collections; 9 | import java.util.List; 10 | 11 | import javax.inject.Inject; 12 | 13 | import io.reactivex.android.schedulers.AndroidSchedulers; 14 | import io.reactivex.disposables.Disposable; 15 | import io.reactivex.schedulers.Schedulers; 16 | import it.codingjam.lifecyclebinder.BindLifeCycle; 17 | import it.cosenonjaviste.core.Navigator; 18 | import it.cosenonjaviste.core.list.RxListViewModel; 19 | import it.cosenonjaviste.core.post.PostListArgument; 20 | import it.cosenonjaviste.model.Author; 21 | 22 | public class AuthorListViewModel extends RxListViewModel { 23 | 24 | @Inject Store, Integer> authorsStore; 25 | 26 | @Inject @BindLifeCycle Navigator navigator; 27 | 28 | @Inject public AuthorListViewModel() { 29 | } 30 | 31 | @NonNull @Override protected AuthorListModel createModel() { 32 | return new AuthorListModel(); 33 | } 34 | 35 | @Override protected Disposable reloadData(ObservableBoolean loadingAction, boolean forceFetch) { 36 | loadingAction.set(true); 37 | return (forceFetch ? authorsStore.fetch(0) : authorsStore.get(0)) 38 | .singleOrError() 39 | .doOnSuccess(Collections::sort) 40 | .subscribeOn(Schedulers.io()) 41 | .observeOn(AndroidSchedulers.mainThread()) 42 | .doAfterTerminate(() -> loadingAction.set(false)) 43 | .subscribe(model::done, throwable -> model.error()); 44 | } 45 | 46 | public void goToAuthorDetail(int position) { 47 | Author author = model.get(position); 48 | navigator.openPostList(PostListArgument.create(author)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/base/ArgumentManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Fabio Collini. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package it.cosenonjaviste.core.base; 17 | 18 | import android.content.Intent; 19 | import android.os.Bundle; 20 | import android.os.Parcelable; 21 | 22 | public class ArgumentManager { 23 | public static final String ARGUMENT = "argument"; 24 | 25 | public static

P readArgument(Bundle arguments) { 26 | return arguments != null ? arguments.getParcelable(ARGUMENT) : null; 27 | } 28 | 29 | public static Intent writeArgument(Intent intent, Parcelable argument) { 30 | if (argument != null) { 31 | intent.putExtra(ARGUMENT, argument); 32 | } 33 | return intent; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/base/RxViewModel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Fabio Collini. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package it.cosenonjaviste.core.base; 17 | 18 | 19 | import android.os.Parcelable; 20 | import android.support.v4.app.Fragment; 21 | 22 | import io.reactivex.disposables.CompositeDisposable; 23 | 24 | public abstract class RxViewModel extends ViewModel { 25 | 26 | protected final CompositeDisposable disposable = new CompositeDisposable(); 27 | 28 | @Override public void onDestroy(Fragment view, boolean changingConfigurations) { 29 | if (!changingConfigurations) { 30 | disposable.clear(); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/base/ViewModel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Fabio Collini. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package it.cosenonjaviste.core.base; 17 | 18 | import android.content.Intent; 19 | import android.os.Bundle; 20 | import android.os.Parcelable; 21 | import android.support.annotation.NonNull; 22 | import android.support.v4.app.Fragment; 23 | 24 | import it.codingjam.lifecyclebinder.DefaultLifeCycleAware; 25 | 26 | public abstract class ViewModel extends DefaultLifeCycleAware { 27 | 28 | public static final String MODEL = "model"; 29 | 30 | protected M model; 31 | 32 | protected A argument; 33 | 34 | @Override public void onCreate(Fragment view, Bundle state, Intent intent, Bundle a) { 35 | M model = null; 36 | if (state != null) { 37 | model = state.getParcelable(ViewModel.MODEL); 38 | } 39 | 40 | initArgumentAndModel(ArgumentManager.readArgument(a), model); 41 | } 42 | 43 | @Override public void onPause(Fragment view) { 44 | pause(); 45 | } 46 | 47 | public void pause() { 48 | } 49 | 50 | @Override public void onResume(Fragment view) { 51 | resume(); 52 | } 53 | 54 | public void resume() { 55 | } 56 | 57 | @NonNull protected abstract M createModel(); 58 | 59 | public void initArgumentAndModel(A arguments, M model) { 60 | this.argument = arguments; 61 | this.model = model != null ? model : createModel(); 62 | } 63 | 64 | public M initAndResume() { 65 | return initAndResume(null); 66 | } 67 | 68 | public M initAndResume(A arguments) { 69 | initArgumentAndModel(arguments, null); 70 | resume(); 71 | return model; 72 | } 73 | 74 | public M getModel() { 75 | return model; 76 | } 77 | 78 | public A getArgument() { 79 | return argument; 80 | } 81 | 82 | @Override public void onSaveInstanceState(Fragment view, Bundle bundle) { 83 | bundle.putParcelable(ViewModel.MODEL, getModel()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/category/CategoryListModel.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.core.category; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import com.hannesdorfmann.parcelableplease.annotation.ParcelablePlease; 7 | 8 | import it.cosenonjaviste.core.list.ListModel; 9 | import it.cosenonjaviste.model.Category; 10 | 11 | @ParcelablePlease 12 | public class CategoryListModel extends ListModel implements Parcelable { 13 | 14 | public Category get(int index) { 15 | return items.get(index); 16 | } 17 | 18 | @Override public int describeContents() { 19 | return 0; 20 | } 21 | 22 | @Override public void writeToParcel(Parcel dest, int flags) { 23 | CategoryListModelParcelablePlease.writeToParcel(this, dest, flags); 24 | } 25 | 26 | public static final Creator CREATOR = new Creator() { 27 | public CategoryListModel createFromParcel(Parcel source) { 28 | CategoryListModel target = new CategoryListModel(); 29 | CategoryListModelParcelablePlease.readFromParcel(target, source); 30 | return target; 31 | } 32 | 33 | public CategoryListModel[] newArray(int size) { 34 | return new CategoryListModel[size]; 35 | } 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/category/CategoryListViewModel.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.core.category; 2 | 3 | import android.databinding.ObservableBoolean; 4 | import android.support.annotation.NonNull; 5 | 6 | import javax.inject.Inject; 7 | 8 | import io.reactivex.android.schedulers.AndroidSchedulers; 9 | import io.reactivex.disposables.Disposable; 10 | import io.reactivex.schedulers.Schedulers; 11 | import it.codingjam.lifecyclebinder.BindLifeCycle; 12 | import it.cosenonjaviste.core.Navigator; 13 | import it.cosenonjaviste.core.list.RxListViewModel; 14 | import it.cosenonjaviste.core.post.PostListArgument; 15 | import it.cosenonjaviste.model.Category; 16 | import it.cosenonjaviste.model.WordPressService; 17 | 18 | public class CategoryListViewModel extends RxListViewModel { 19 | 20 | @Inject WordPressService wordPressService; 21 | 22 | @Inject @BindLifeCycle Navigator navigator; 23 | 24 | @Inject public CategoryListViewModel() { 25 | } 26 | 27 | @NonNull @Override protected CategoryListModel createModel() { 28 | return new CategoryListModel(); 29 | } 30 | 31 | @Override protected Disposable reloadData(ObservableBoolean loadingSetter, boolean forceFetch) { 32 | loadingSetter.set(true); 33 | return wordPressService 34 | .listCategories() 35 | .subscribeOn(Schedulers.io()) 36 | .observeOn(AndroidSchedulers.mainThread()) 37 | .doAfterTerminate(() -> loadingSetter.set(false)) 38 | .subscribe(model::done, throwable -> model.error()); 39 | } 40 | 41 | public void goToPosts(int position) { 42 | Category category = model.get(position); 43 | navigator.openPostList(PostListArgument.create(category)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/contact/ContactModel.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.core.contact; 2 | 3 | import android.databinding.ObservableField; 4 | import android.databinding.ObservableInt; 5 | import android.os.Parcel; 6 | import android.os.Parcelable; 7 | 8 | import com.hannesdorfmann.parcelableplease.annotation.ParcelablePlease; 9 | 10 | @ParcelablePlease 11 | public class ContactModel implements Parcelable { 12 | 13 | public boolean sendPressed; 14 | 15 | public ObservableField name = new ObservableField<>(); 16 | public ObservableField email = new ObservableField<>(); 17 | public ObservableField message = new ObservableField<>(); 18 | 19 | public ObservableInt nameError = new ObservableInt(); 20 | public ObservableInt emailError = new ObservableInt(); 21 | public ObservableInt messageError = new ObservableInt(); 22 | 23 | @Override public int describeContents() { 24 | return 0; 25 | } 26 | 27 | @Override public void writeToParcel(Parcel dest, int flags) { 28 | ContactModelParcelablePlease.writeToParcel(this, dest, flags); 29 | } 30 | 31 | public static final Creator CREATOR = new Creator() { 32 | public ContactModel createFromParcel(Parcel source) { 33 | ContactModel target = new ContactModel(); 34 | ContactModelParcelablePlease.readFromParcel(target, source); 35 | return target; 36 | } 37 | 38 | public ContactModel[] newArray(int size) { 39 | return new ContactModel[size]; 40 | } 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/contact/ContactViewModel.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.core.contact; 2 | 3 | import android.databinding.Observable.OnPropertyChangedCallback; 4 | import android.databinding.ObservableBoolean; 5 | import android.databinding.ObservableInt; 6 | import android.support.annotation.NonNull; 7 | 8 | import javax.inject.Inject; 9 | 10 | import io.reactivex.Completable; 11 | import io.reactivex.android.schedulers.AndroidSchedulers; 12 | import io.reactivex.schedulers.Schedulers; 13 | import it.codingjam.lifecyclebinder.BindLifeCycle; 14 | import it.cosenonjaviste.R; 15 | import it.cosenonjaviste.core.Navigator; 16 | import it.cosenonjaviste.core.base.RxViewModel; 17 | import it.cosenonjaviste.core.utils.EmailVerifier; 18 | import it.cosenonjaviste.model.MailJetService; 19 | 20 | public class ContactViewModel extends RxViewModel { 21 | 22 | @Inject MailJetService mailJetService; 23 | 24 | @Inject @BindLifeCycle Navigator navigator; 25 | 26 | public final ObservableBoolean sending = new ObservableBoolean(); 27 | 28 | private OnPropertyChangedCallback listener = new OnPropertyChangedCallback() { 29 | @Override 30 | public void onPropertyChanged(android.databinding.Observable sender, int propertyId) { 31 | validate(); 32 | } 33 | }; 34 | 35 | @Inject public ContactViewModel() { 36 | } 37 | 38 | @NonNull @Override protected ContactModel createModel() { 39 | return new ContactModel(); 40 | } 41 | 42 | @Override public void resume() { 43 | super.resume(); 44 | 45 | model.name.addOnPropertyChangedCallback(listener); 46 | model.message.addOnPropertyChangedCallback(listener); 47 | model.email.addOnPropertyChangedCallback(listener); 48 | } 49 | 50 | @Override public void pause() { 51 | super.pause(); 52 | model.name.removeOnPropertyChangedCallback(listener); 53 | model.message.removeOnPropertyChangedCallback(listener); 54 | model.email.removeOnPropertyChangedCallback(listener); 55 | } 56 | 57 | private boolean validate() { 58 | if (model.sendPressed) { 59 | boolean isValid = checkMandatory(model.nameError, model.name.get() == null || model.name.get().isEmpty()); 60 | if (model.email.get() != null && !model.email.get().isEmpty()) { 61 | if (!EmailVerifier.checkEmail(model.email.get())) { 62 | model.emailError.set(R.string.invalid_email); 63 | isValid = false; 64 | } else { 65 | model.emailError.set(0); 66 | } 67 | } else { 68 | model.emailError.set(R.string.mandatory_field); 69 | isValid = false; 70 | } 71 | isValid = checkMandatory(model.messageError, model.message.get() == null || model.message.get().isEmpty()) && isValid; 72 | return isValid; 73 | } else { 74 | return true; 75 | } 76 | } 77 | 78 | private boolean checkMandatory(ObservableInt error, boolean empty) { 79 | error.set(empty ? R.string.mandatory_field : 0); 80 | return !empty; 81 | } 82 | 83 | public void send() { 84 | model.sendPressed = true; 85 | if (validate()) { 86 | Completable observable = mailJetService.sendEmail( 87 | model.name + " ", 88 | "info@cosenonjaviste.it", 89 | "Email from " + model.name, 90 | "Reply to: " + model.email + "\n" + model.message 91 | ); 92 | 93 | sending.set(true); 94 | disposable.add(observable 95 | .subscribeOn(Schedulers.io()) 96 | .observeOn(AndroidSchedulers.mainThread()) 97 | .doAfterTerminate(() -> sending.set(false)) 98 | .subscribe( 99 | () -> navigator.showMessage(R.string.message_sent), 100 | t -> navigator.showMessage(R.string.error_sending_message)) 101 | ); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/list/GenericRxListViewModel.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.core.list; 2 | 3 | import android.databinding.ObservableBoolean; 4 | 5 | public interface GenericRxListViewModel { 6 | ObservableBoolean isLoading(); 7 | 8 | ObservableBoolean isLoadingPullToRefresh(); 9 | 10 | ObservableBoolean isLoadingNextPage(); 11 | 12 | ObservableBoolean isError(); 13 | 14 | void reloadData(); 15 | 16 | void loadDataPullToRefresh(); 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/list/ListModel.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.core.list; 2 | 3 | import android.databinding.ObservableArrayList; 4 | import android.databinding.ObservableBoolean; 5 | import android.os.Parcelable; 6 | 7 | import com.hannesdorfmann.parcelableplease.annotation.Bagger; 8 | 9 | import java.util.List; 10 | 11 | import it.cosenonjaviste.core.utils.ObservableArrayListBagger; 12 | 13 | public abstract class ListModel implements Parcelable { 14 | 15 | public ObservableBoolean error = new ObservableBoolean(); 16 | 17 | public boolean loaded; 18 | 19 | @Bagger(ObservableArrayListBagger.class) 20 | public ObservableArrayList items = new ObservableArrayList<>(); 21 | 22 | public final ObservableArrayList getItems() { 23 | return items; 24 | } 25 | 26 | public boolean isLoaded() { 27 | return loaded || error.get(); 28 | } 29 | 30 | public void clear() { 31 | getItems().clear(); 32 | } 33 | 34 | public void done(List items) { 35 | loaded = true; 36 | getItems().clear(); 37 | getItems().addAll(items); 38 | error.set(false); 39 | } 40 | 41 | public void append(List items) { 42 | loaded = true; 43 | getItems().addAll(items); 44 | error.set(false); 45 | } 46 | 47 | public void error() { 48 | clear(); 49 | error.set(true); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/list/RxListViewModel.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.core.list; 2 | 3 | import android.databinding.ObservableBoolean; 4 | 5 | import io.reactivex.disposables.Disposable; 6 | import it.cosenonjaviste.core.base.RxViewModel; 7 | 8 | public abstract class RxListViewModel> extends RxViewModel implements GenericRxListViewModel { 9 | protected ObservableBoolean loading = new ObservableBoolean(); 10 | 11 | protected ObservableBoolean loadingNextPage = new ObservableBoolean(); 12 | 13 | protected ObservableBoolean loadingPullToRefresh = new ObservableBoolean(); 14 | 15 | @Override public ObservableBoolean isLoading() { 16 | return loading; 17 | } 18 | 19 | @Override public ObservableBoolean isLoadingPullToRefresh() { 20 | return loadingPullToRefresh; 21 | } 22 | 23 | @Override public ObservableBoolean isLoadingNextPage() { 24 | return loadingNextPage; 25 | } 26 | 27 | @Override public ObservableBoolean isError() { 28 | return model.error; 29 | } 30 | 31 | @Override public void resume() { 32 | super.resume(); 33 | reloadData(); 34 | } 35 | 36 | public void reloadData() { 37 | reloadData(loading, false); 38 | } 39 | 40 | public final void loadDataPullToRefresh() { 41 | reloadData(loadingPullToRefresh, true); 42 | } 43 | 44 | protected abstract Disposable reloadData(ObservableBoolean loadingAction, boolean forceFetch); 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/page/PageModel.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.core.page; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import com.hannesdorfmann.parcelableplease.annotation.ParcelablePlease; 7 | 8 | import it.cosenonjaviste.model.Post; 9 | 10 | @ParcelablePlease 11 | public class PageModel implements Parcelable { 12 | 13 | Post post; 14 | 15 | public PageModel() { 16 | } 17 | 18 | public Post getPost() { 19 | return post; 20 | } 21 | 22 | public void setPost(Post post) { 23 | this.post = post; 24 | } 25 | 26 | @Override public int describeContents() { 27 | return 0; 28 | } 29 | 30 | @Override public void writeToParcel(Parcel dest, int flags) { 31 | PageModelParcelablePlease.writeToParcel(this, dest, flags); 32 | } 33 | 34 | public static final Creator CREATOR = new Creator() { 35 | public PageModel createFromParcel(Parcel source) { 36 | PageModel target = new PageModel(); 37 | PageModelParcelablePlease.readFromParcel(target, source); 38 | return target; 39 | } 40 | 41 | public PageModel[] newArray(int size) { 42 | return new PageModel[size]; 43 | } 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/page/PageViewModel.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.core.page; 2 | 3 | import android.databinding.ObservableBoolean; 4 | import android.support.annotation.NonNull; 5 | 6 | import javax.inject.Inject; 7 | 8 | import it.codingjam.lifecyclebinder.BindLifeCycle; 9 | import it.cosenonjaviste.core.Navigator; 10 | import it.cosenonjaviste.core.base.ViewModel; 11 | import it.cosenonjaviste.model.Post; 12 | 13 | public class PageViewModel extends ViewModel { 14 | 15 | public ObservableBoolean loading = new ObservableBoolean(); 16 | 17 | @Inject @BindLifeCycle Navigator navigator; 18 | 19 | @Inject public PageViewModel() { 20 | } 21 | 22 | @NonNull @Override protected PageModel createModel() { 23 | return new PageModel(); 24 | } 25 | 26 | public Post getPost() { 27 | return model.getPost(); 28 | } 29 | 30 | @Override public void resume() { 31 | super.resume(); 32 | if (model.getPost() == null) { 33 | model.setPost(getArgument()); 34 | loading.set(true); 35 | } 36 | } 37 | 38 | public void htmlLoaded() { 39 | loading.set(false); 40 | } 41 | 42 | public void share() { 43 | Post post = model.getPost(); 44 | navigator.share(post.title(), post.title() + " - " + post.url()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/post/PostListArgument.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.core.post; 2 | 3 | import android.os.Parcelable; 4 | import android.support.annotation.Nullable; 5 | 6 | import com.google.auto.value.AutoValue; 7 | 8 | import it.cosenonjaviste.model.Author; 9 | import it.cosenonjaviste.model.Category; 10 | 11 | @AutoValue 12 | public abstract class PostListArgument implements Parcelable { 13 | 14 | public static PostListArgument create(Category category) { 15 | return new AutoValue_PostListArgument(category, null); 16 | } 17 | 18 | public static PostListArgument create(Author author) { 19 | return new AutoValue_PostListArgument(null, author); 20 | } 21 | 22 | @Nullable public abstract Category category(); 23 | 24 | @Nullable public abstract Author author(); 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/post/PostListModel.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.core.post; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import com.hannesdorfmann.parcelableplease.annotation.ParcelablePlease; 7 | 8 | import it.cosenonjaviste.core.list.ListModel; 9 | import it.cosenonjaviste.model.Post; 10 | 11 | @ParcelablePlease 12 | public class PostListModel extends ListModel implements Parcelable { 13 | 14 | boolean moreDataAvailable; 15 | 16 | public PostListModel() { 17 | } 18 | 19 | public boolean isMoreDataAvailable() { 20 | return moreDataAvailable; 21 | } 22 | 23 | public void setMoreDataAvailable(boolean moreDataAvailable) { 24 | this.moreDataAvailable = moreDataAvailable; 25 | } 26 | 27 | public int size() { 28 | return items.size(); 29 | } 30 | 31 | @Override public int describeContents() { 32 | return 0; 33 | } 34 | 35 | @Override public void writeToParcel(Parcel dest, int flags) { 36 | PostListModelParcelablePlease.writeToParcel(this, dest, flags); 37 | } 38 | 39 | public static final Creator CREATOR = new Creator() { 40 | public PostListModel createFromParcel(Parcel source) { 41 | PostListModel target = new PostListModel(); 42 | PostListModelParcelablePlease.readFromParcel(target, source); 43 | return target; 44 | } 45 | 46 | public PostListModel[] newArray(int size) { 47 | return new PostListModel[size]; 48 | } 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/post/PostListViewModel.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.core.post; 2 | 3 | import android.databinding.ObservableBoolean; 4 | import android.support.annotation.NonNull; 5 | 6 | import java.util.List; 7 | 8 | import javax.inject.Inject; 9 | 10 | import io.reactivex.Single; 11 | import io.reactivex.android.schedulers.AndroidSchedulers; 12 | import io.reactivex.disposables.Disposable; 13 | import io.reactivex.schedulers.Schedulers; 14 | import it.codingjam.lifecyclebinder.BindLifeCycle; 15 | import it.cosenonjaviste.core.Navigator; 16 | import it.cosenonjaviste.core.list.RxListViewModel; 17 | import it.cosenonjaviste.model.Author; 18 | import it.cosenonjaviste.model.Category; 19 | import it.cosenonjaviste.model.Post; 20 | import it.cosenonjaviste.model.WordPressService; 21 | 22 | public class PostListViewModel extends RxListViewModel { 23 | 24 | @Inject WordPressService wordPressService; 25 | 26 | @Inject @BindLifeCycle Navigator navigator; 27 | 28 | @Inject public PostListViewModel() { 29 | } 30 | 31 | @NonNull @Override protected PostListModel createModel() { 32 | return new PostListModel(); 33 | } 34 | 35 | @Override protected Disposable reloadData(ObservableBoolean loadingAction, boolean forceFetch) { 36 | loadingAction.set(true); 37 | return getObservable(1) 38 | .subscribeOn(Schedulers.io()) 39 | .observeOn(AndroidSchedulers.mainThread()) 40 | .doAfterTerminate(() -> loadingAction.set(false)) 41 | .subscribe(posts -> { 42 | model.done(posts); 43 | model.setMoreDataAvailable(posts.size() == WordPressService.POST_PAGE_SIZE); 44 | }, throwable -> model.error()); 45 | } 46 | 47 | public void goToDetail(int position) { 48 | Post item = model.getItems().get(position); 49 | navigator.openDetail(item); 50 | } 51 | 52 | public void loadNextPage() { 53 | if (!loadingNextPage.get() && model.isMoreDataAvailable()) { 54 | int page = calcNextPage(model.getItems().size(), WordPressService.POST_PAGE_SIZE); 55 | 56 | loadingNextPage.set(true); 57 | disposable.add(getObservable(page) 58 | .subscribeOn(Schedulers.io()) 59 | .observeOn(AndroidSchedulers.mainThread()) 60 | .doAfterTerminate(() -> loadingNextPage.set(false)) 61 | .subscribe(posts -> { 62 | model.append(posts); 63 | model.setMoreDataAvailable(posts.size() == WordPressService.POST_PAGE_SIZE); 64 | }, throwable -> model.error()) 65 | ); 66 | } 67 | } 68 | 69 | private Single> getObservable(int page) { 70 | if (getArgument() == null) { 71 | return wordPressService.listPosts(page); 72 | } else { 73 | Category category = getArgument().category(); 74 | if (category != null) { 75 | return wordPressService.listCategoryPosts(category.id(), page); 76 | } else { 77 | Author author = getArgument().author(); 78 | return wordPressService.listAuthorPosts(author.id(), page); 79 | } 80 | } 81 | } 82 | 83 | private static int calcNextPage(int size, int pageSize) { 84 | return size / pageSize + 1; 85 | } 86 | 87 | public boolean isToolbarVisible() { 88 | PostListArgument arg = getArgument(); 89 | return arg != null && (arg.author() != null || arg.category() != null); 90 | } 91 | 92 | public String getToolbarTitle() { 93 | PostListArgument arg = getArgument(); 94 | if (arg == null) { 95 | return null; 96 | } else { 97 | Author author = arg.author(); 98 | if (author != null) { 99 | return author.name(); 100 | } else { 101 | Category category = arg.category(); 102 | if (category != null) { 103 | return category.title(); 104 | } else { 105 | return null; 106 | } 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/twitter/TweetListModel.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.core.twitter; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import com.hannesdorfmann.parcelableplease.annotation.ParcelablePlease; 7 | 8 | import it.cosenonjaviste.core.list.ListModel; 9 | import it.cosenonjaviste.model.Tweet; 10 | 11 | @ParcelablePlease 12 | public class TweetListModel extends ListModel implements Parcelable { 13 | 14 | boolean moreDataAvailable; 15 | 16 | public void setMoreDataAvailable(boolean moreDataAvailable) { 17 | this.moreDataAvailable = moreDataAvailable; 18 | } 19 | 20 | public boolean isMoreDataAvailable() { 21 | return moreDataAvailable; 22 | } 23 | 24 | @Override public int describeContents() { 25 | return 0; 26 | } 27 | 28 | @Override public void writeToParcel(Parcel dest, int flags) { 29 | TweetListModelParcelablePlease.writeToParcel(this, dest, flags); 30 | } 31 | 32 | public static final Creator CREATOR = new Creator() { 33 | public TweetListModel createFromParcel(Parcel source) { 34 | TweetListModel target = new TweetListModel(); 35 | TweetListModelParcelablePlease.readFromParcel(target, source); 36 | return target; 37 | } 38 | 39 | public TweetListModel[] newArray(int size) { 40 | return new TweetListModel[size]; 41 | } 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/twitter/TweetListViewModel.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.core.twitter; 2 | 3 | import android.databinding.ObservableBoolean; 4 | import android.support.annotation.NonNull; 5 | 6 | import javax.inject.Inject; 7 | 8 | import io.reactivex.android.schedulers.AndroidSchedulers; 9 | import io.reactivex.disposables.Disposable; 10 | import io.reactivex.schedulers.Schedulers; 11 | import it.cosenonjaviste.core.list.RxListViewModel; 12 | import it.cosenonjaviste.model.TwitterService; 13 | 14 | public class TweetListViewModel extends RxListViewModel { 15 | 16 | @Inject TwitterService twitterService; 17 | 18 | @Inject public TweetListViewModel() { 19 | } 20 | 21 | @NonNull @Override protected TweetListModel createModel() { 22 | return new TweetListModel(); 23 | } 24 | 25 | @Override protected Disposable reloadData(ObservableBoolean loadingAction, boolean forceFetch) { 26 | loadingAction.set(true); 27 | return twitterService.loadTweets(1) 28 | .subscribeOn(Schedulers.io()) 29 | .observeOn(AndroidSchedulers.mainThread()) 30 | .doAfterTerminate(() -> loadingAction.set(false)) 31 | .subscribe(posts -> { 32 | model.done(posts); 33 | model.setMoreDataAvailable(posts.size() == TwitterService.PAGE_SIZE); 34 | }, throwable -> model.error()); 35 | } 36 | 37 | public void loadNextPage() { 38 | if (!isLoadingNextPage().get() && model.isMoreDataAvailable()) { 39 | int page = calcNextPage(model.getItems().size(), TwitterService.PAGE_SIZE); 40 | 41 | loadingNextPage.set(true); 42 | disposable.add(twitterService.loadTweets(page) 43 | .subscribeOn(Schedulers.io()) 44 | .observeOn(AndroidSchedulers.mainThread()) 45 | .doAfterTerminate(() -> loadingNextPage.set(false)) 46 | .subscribe(posts -> { 47 | model.append(posts); 48 | model.setMoreDataAvailable(posts.size() == TwitterService.PAGE_SIZE); 49 | }, throwable -> model.error()) 50 | ); 51 | } 52 | } 53 | 54 | private static int calcNextPage(int size, int pageSize) { 55 | return size / pageSize + 1; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/utils/DenvelopingConverter.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.core.utils; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.annotation.Nullable; 5 | 6 | import com.google.gson.Gson; 7 | import com.google.gson.TypeAdapter; 8 | import com.google.gson.reflect.TypeToken; 9 | import com.google.gson.stream.JsonReader; 10 | 11 | import java.io.IOException; 12 | import java.lang.annotation.Annotation; 13 | import java.lang.reflect.Type; 14 | 15 | import okhttp3.ResponseBody; 16 | import retrofit2.Converter; 17 | import retrofit2.Retrofit; 18 | 19 | /** 20 | * A {@link retrofit2.Converter.Factory} which removes unwanted wrapping envelopes from API 21 | * responses. 22 | */ 23 | public class DenvelopingConverter extends Converter.Factory { 24 | 25 | final Gson gson; 26 | 27 | public DenvelopingConverter(@NonNull Gson gson) { 28 | this.gson = gson; 29 | } 30 | 31 | @Override 32 | public Converter responseBodyConverter( 33 | Type type, Annotation[] annotations, Retrofit retrofit) { 34 | 35 | // This converter requires an annotation providing the name of the payload in the envelope; 36 | // if one is not supplied then return null to continue down the converter chain. 37 | final String payloadName = getPayloadName(annotations); 38 | if (payloadName == null) return null; 39 | 40 | final TypeAdapter adapter = gson.getAdapter(TypeToken.get(type)); 41 | return new Converter() { 42 | @Override 43 | public Object convert(ResponseBody body) throws IOException { 44 | try { 45 | JsonReader jsonReader = gson.newJsonReader(body.charStream()); 46 | jsonReader.beginObject(); 47 | while (jsonReader.hasNext()) { 48 | if (payloadName.equals(jsonReader.nextName())) { 49 | return adapter.read(jsonReader); 50 | } else { 51 | jsonReader.skipValue(); 52 | } 53 | } 54 | return null; 55 | } finally { 56 | body.close(); 57 | } 58 | } 59 | }; 60 | } 61 | 62 | private @Nullable String getPayloadName(Annotation[] annotations) { 63 | if (annotations == null) { 64 | return null; 65 | } 66 | for (Annotation annotation : annotations) { 67 | if (annotation instanceof EnvelopePayload) { 68 | return ((EnvelopePayload) annotation).value(); 69 | } 70 | } 71 | return null; 72 | } 73 | } -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/utils/EmailVerifier.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.core.utils; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | public class EmailVerifier { 6 | private static final Pattern EMAIL_ADDRESS 7 | = Pattern.compile( 8 | "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" + 9 | "\\@" + 10 | "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + 11 | "(" + 12 | "\\." + 13 | "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + 14 | ")+" 15 | ); 16 | 17 | public static boolean checkEmail(String email) { 18 | return EMAIL_ADDRESS.matcher(email).matches(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/utils/EnvelopePayload.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.core.utils; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.METHOD; 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | /** 10 | * An annotation for identifying the payload that we want to extract from an API response wrapped in 11 | * an envelope object. 12 | */ 13 | @Target(METHOD) 14 | @Retention(RUNTIME) 15 | public @interface EnvelopePayload { 16 | String value() default ""; 17 | } -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/utils/Md5Utils.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.core.utils; 2 | 3 | import java.security.MessageDigest; 4 | 5 | public class Md5Utils { 6 | private static String hex(byte[] array) { 7 | StringBuilder sb = new StringBuilder(); 8 | for (byte anArray : array) { 9 | sb.append(Integer.toHexString((anArray & 0xFF) | 0x100).substring(1, 3)); 10 | } 11 | return sb.toString(); 12 | } 13 | 14 | public static String md5Hex(String message) { 15 | try { 16 | MessageDigest md = MessageDigest.getInstance("MD5"); 17 | return hex(md.digest(message.getBytes("CP1252"))); 18 | } catch (Exception ignored) { 19 | } 20 | return null; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/core/utils/ObservableArrayListBagger.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.core.utils; 2 | 3 | import android.databinding.ObservableArrayList; 4 | import android.os.Parcel; 5 | 6 | import com.hannesdorfmann.parcelableplease.ParcelBagger; 7 | 8 | public class ObservableArrayListBagger implements ParcelBagger { 9 | @Override public void write(ObservableArrayList value, Parcel out, int flags) { 10 | out.writeList(value); 11 | } 12 | 13 | @Override public ObservableArrayList read(Parcel in) { 14 | ObservableArrayList observableArrayList = new ObservableArrayList(); 15 | in.readList(observableArrayList, observableArrayList.getClass().getClassLoader()); 16 | return observableArrayList; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/model/Attachment.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.model; 2 | 3 | import android.os.Parcelable; 4 | 5 | import com.google.auto.value.AutoValue; 6 | import com.google.gson.Gson; 7 | import com.google.gson.TypeAdapter; 8 | 9 | @AutoValue 10 | public abstract class Attachment implements Parcelable { 11 | public static Attachment create(String url) { 12 | return new AutoValue_Attachment(url); 13 | } 14 | 15 | public abstract String url(); 16 | 17 | public static TypeAdapter typeAdapter(Gson gson) { 18 | return new AutoValue_Attachment.GsonTypeAdapter(gson); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/model/Author.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.model; 2 | 3 | import android.os.Parcelable; 4 | 5 | import com.google.auto.value.AutoValue; 6 | import com.google.gson.Gson; 7 | import com.google.gson.TypeAdapter; 8 | import com.google.gson.annotations.SerializedName; 9 | 10 | import it.cosenonjaviste.core.utils.Md5Utils; 11 | 12 | @AutoValue 13 | public abstract class Author implements Comparable, Parcelable { 14 | 15 | public static Author create(long id, String firstName, String lastName, String email) { 16 | return new AutoValue_Author(id, firstName, lastName, email); 17 | } 18 | 19 | public static TypeAdapter typeAdapter(Gson gson) { 20 | return new AutoValue_Author.GsonTypeAdapter(gson); 21 | } 22 | 23 | public abstract long id(); 24 | 25 | @SerializedName("first_name") 26 | public abstract String firstName(); 27 | 28 | @SerializedName("last_name") 29 | public abstract String lastName(); 30 | 31 | public abstract String email(); 32 | 33 | public String name() { 34 | return firstName() + " " + lastName(); 35 | } 36 | 37 | public String imageUrl() { 38 | if (email() != null && email().length() > 0) { 39 | return "http://www.gravatar.com/avatar/" + Md5Utils.md5Hex(email()); 40 | } 41 | return null; 42 | } 43 | 44 | @Override public int compareTo(Author o) { 45 | long lhs = sortId(); 46 | long rhs = o.sortId(); 47 | return lhs < rhs ? -1 : (lhs == rhs ? 0 : 1); 48 | } 49 | 50 | private long sortId() { 51 | long id = id(); 52 | if (id < 5 || id == 8 || id == 32) { 53 | return id; 54 | } else { 55 | return id + 100; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/model/Category.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.model; 2 | 3 | import android.os.Parcelable; 4 | 5 | import com.google.auto.value.AutoValue; 6 | import com.google.gson.Gson; 7 | import com.google.gson.TypeAdapter; 8 | import com.google.gson.annotations.SerializedName; 9 | 10 | @AutoValue 11 | public abstract class Category implements Parcelable { 12 | 13 | public static Category create(long id, String title, int postCount) { 14 | return new AutoValue_Category(id, title, postCount); 15 | } 16 | 17 | public abstract long id(); 18 | 19 | public abstract String title(); 20 | 21 | @SerializedName("post_count") 22 | public abstract int postCount(); 23 | 24 | public static TypeAdapter typeAdapter(Gson gson) { 25 | return new AutoValue_Category.GsonTypeAdapter(gson); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/model/MailJetService.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.model; 2 | 3 | 4 | import io.reactivex.Completable; 5 | import retrofit2.http.Field; 6 | import retrofit2.http.FormUrlEncoded; 7 | import retrofit2.http.POST; 8 | 9 | public interface MailJetService { 10 | 11 | @POST("/send/message") @FormUrlEncoded Completable sendEmail( 12 | @Field("from") String from, 13 | @Field("to") String to, 14 | @Field("subject") String subject, 15 | @Field("text") String text 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/model/MyAdapterFactory.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.model; 2 | 3 | import com.google.gson.TypeAdapterFactory; 4 | import com.ryanharter.auto.value.gson.GsonTypeAdapterFactory; 5 | 6 | @GsonTypeAdapterFactory 7 | public abstract class MyAdapterFactory implements TypeAdapterFactory { 8 | 9 | public static TypeAdapterFactory create() { 10 | return new AutoValueGson_MyAdapterFactory(); 11 | } 12 | } -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/model/Post.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.model; 2 | 3 | import android.os.Parcelable; 4 | import android.support.annotation.Nullable; 5 | 6 | import com.google.auto.value.AutoValue; 7 | import com.google.gson.Gson; 8 | import com.google.gson.TypeAdapter; 9 | 10 | import java.util.Arrays; 11 | import java.util.Date; 12 | import java.util.List; 13 | 14 | import it.cosenonjaviste.ui.utils.DateFormatter; 15 | 16 | @AutoValue 17 | public abstract class Post implements Parcelable { 18 | public static Post create(long id, Author author, String title, Date date, String url, String excerpt, Attachment... attachments) { 19 | return new AutoValue_Post(id, author, title, date, url, excerpt, Arrays.asList(attachments)); 20 | } 21 | 22 | public abstract long id(); 23 | 24 | public abstract Author author(); 25 | 26 | public abstract String title(); 27 | 28 | @Nullable 29 | public abstract Date date(); 30 | 31 | public abstract String url(); 32 | 33 | @Nullable 34 | public abstract String excerpt(); 35 | 36 | public abstract List attachments(); 37 | 38 | public String excerptHtml() { 39 | String excerpt = excerpt(); 40 | if (excerpt == null) { 41 | return ""; 42 | } 43 | return excerpt.replaceAll("
Continue reading...<\\/a>", "").replaceAll("^

", "").replaceAll("$

", ""); 44 | } 45 | 46 | public String subtitle() { 47 | return author().name() + ", " + DateFormatter.formatDate(date()); 48 | } 49 | 50 | public String imageUrl() { 51 | if (attachments() != null && !attachments().isEmpty()) { 52 | return attachments().get(0).url(); 53 | } else { 54 | return null; 55 | } 56 | } 57 | 58 | public static TypeAdapter typeAdapter(Gson gson) { 59 | return new AutoValue_Post.GsonTypeAdapter(gson); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/model/Tweet.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.model; 2 | 3 | import android.os.Parcelable; 4 | 5 | import com.google.auto.value.AutoValue; 6 | import com.google.gson.Gson; 7 | import com.google.gson.TypeAdapter; 8 | 9 | import java.util.Date; 10 | 11 | @AutoValue 12 | public abstract class Tweet implements Parcelable { 13 | public static Tweet create(long id, String text, Date createdAt, String userImage, String author) { 14 | return new AutoValue_Tweet(id, text, createdAt, userImage, author); 15 | } 16 | 17 | public abstract long id(); 18 | 19 | public abstract String text(); 20 | 21 | public abstract Date createdAt(); 22 | 23 | public abstract String userImage(); 24 | 25 | public abstract String author(); 26 | 27 | public static TypeAdapter typeAdapter(Gson gson) { 28 | return new AutoValue_Tweet.GsonTypeAdapter(gson); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/model/TwitterService.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.model; 2 | 3 | import java.util.List; 4 | 5 | import io.reactivex.Observable; 6 | import io.reactivex.Single; 7 | import twitter4j.Paging; 8 | import twitter4j.Status; 9 | import twitter4j.Twitter; 10 | import twitter4j.TwitterFactory; 11 | import twitter4j.User; 12 | import twitter4j.conf.ConfigurationBuilder; 13 | 14 | public class TwitterService { 15 | 16 | public static final int PAGE_SIZE = 20; 17 | 18 | private final Twitter twitter; 19 | 20 | public TwitterService(String consumerKey, String consumerSecret, String accessToken, String accessTokenSecret) { 21 | ConfigurationBuilder cb = new ConfigurationBuilder(); 22 | cb.setDebugEnabled(true) 23 | .setOAuthConsumerKey(consumerKey) 24 | .setOAuthConsumerSecret(consumerSecret) 25 | .setOAuthAccessToken(accessToken) 26 | .setOAuthAccessTokenSecret(accessTokenSecret); 27 | TwitterFactory tf = new TwitterFactory(cb.build()); 28 | twitter = tf.getInstance(); 29 | } 30 | 31 | public Single> loadTweets(int page) { 32 | return Observable.fromCallable(() -> twitter.getUserTimeline(251259751, new Paging(page, PAGE_SIZE))) 33 | .flatMapIterable(l -> l) 34 | .map(this::createTweet).toList(); 35 | } 36 | 37 | private Tweet createTweet(Status s) { 38 | User user = s.getUser(); 39 | return Tweet.create(s.getId(), s.getText(), s.getCreatedAt(), user.getProfileImageURL(), user.getName()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/model/WordPressService.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.model; 2 | 3 | import java.util.List; 4 | 5 | import io.reactivex.Single; 6 | import it.cosenonjaviste.core.utils.EnvelopePayload; 7 | import retrofit2.http.GET; 8 | import retrofit2.http.Query; 9 | 10 | public interface WordPressService { 11 | 12 | int POST_PAGE_SIZE = 10; 13 | 14 | String POSTS_EXTRA = "&exclude=content,title_plain,tags,custom_fields,categories,comments&author_meta=email"; 15 | String CATEGORY_POSTS_URL = "/?json=get_category_posts"; 16 | String AUTHOR_POSTS_URL = "/?json=get_author_posts"; 17 | 18 | @EnvelopePayload("posts") 19 | @GET("/?json=get_recent_posts&count=" + POST_PAGE_SIZE + POSTS_EXTRA) Single> listPosts(@Query("page") int page); 20 | 21 | @EnvelopePayload("posts") 22 | @GET(CATEGORY_POSTS_URL + "&count=" + POST_PAGE_SIZE + POSTS_EXTRA) Single> listCategoryPosts(@Query("id") long categoryId, @Query("page") int page); 23 | 24 | @EnvelopePayload("posts") 25 | @GET(AUTHOR_POSTS_URL + "&count=" + POST_PAGE_SIZE + POSTS_EXTRA) Single> listAuthorPosts(@Query("id") long authorId, @Query("page") int page); 26 | 27 | @EnvelopePayload("authors") 28 | @GET("/?json=get_author_index&author_meta=email") Single> listAuthors(); 29 | 30 | @EnvelopePayload("categories") 31 | @GET("/?json=get_category_index") Single> listCategories(); 32 | } -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/AndroidNavigator.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.design.widget.Snackbar; 6 | import android.support.v4.app.Fragment; 7 | import android.support.v4.app.FragmentActivity; 8 | 9 | import it.cosenonjaviste.R; 10 | import it.cosenonjaviste.core.Navigator; 11 | import it.cosenonjaviste.core.post.PostListArgument; 12 | import it.cosenonjaviste.model.Post; 13 | import it.cosenonjaviste.ui.page.PageFragment; 14 | import it.cosenonjaviste.ui.post.PostListFragment; 15 | import it.cosenonjaviste.ui.utils.SingleFragmentActivity; 16 | 17 | public class AndroidNavigator extends Navigator { 18 | 19 | private FragmentActivity activity; 20 | 21 | @Override 22 | public void onCreate(Object view, Bundle savedInstanceState, Intent intent, Bundle arguments) { 23 | if (view instanceof Fragment) { 24 | activity = ((Fragment) view).getActivity(); 25 | } else { 26 | activity = (FragmentActivity) view; 27 | } 28 | } 29 | 30 | @Override 31 | public void onDestroy(Object view, boolean changingConfigurations) { 32 | activity = null; 33 | } 34 | 35 | @Override 36 | public void openPostList(PostListArgument argument) { 37 | SingleFragmentActivity.open(activity, PostListFragment.class, argument); 38 | } 39 | 40 | @Override 41 | public void openDetail(Post post) { 42 | SingleFragmentActivity.open(activity, PageFragment.class, post); 43 | } 44 | 45 | @Override 46 | public void share(String subject, String text) { 47 | Intent sendIntent = new Intent(); 48 | sendIntent.setAction(Intent.ACTION_SEND); 49 | sendIntent.putExtra(Intent.EXTRA_SUBJECT, subject); 50 | sendIntent.putExtra(Intent.EXTRA_TEXT, text); 51 | sendIntent.setType("text/plain"); 52 | activity.startActivity(Intent.createChooser(sendIntent, activity.getResources().getText(R.string.share_post))); 53 | } 54 | 55 | @Override public void showMessage(int message) { 56 | Snackbar.make(activity.findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG).show(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/AppModule.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui; 2 | 3 | import android.app.Application; 4 | import android.util.Base64; 5 | 6 | import com.google.gson.Gson; 7 | import com.google.gson.GsonBuilder; 8 | import com.nytimes.android.external.store2.base.impl.Store; 9 | import com.nytimes.android.external.store2.base.impl.StoreBuilder; 10 | 11 | import java.util.List; 12 | 13 | import javax.inject.Singleton; 14 | 15 | import dagger.Module; 16 | import dagger.Provides; 17 | import it.cosenonjaviste.BuildConfig; 18 | import it.cosenonjaviste.core.Navigator; 19 | import it.cosenonjaviste.core.utils.DenvelopingConverter; 20 | import it.cosenonjaviste.model.Author; 21 | import it.cosenonjaviste.model.MailJetService; 22 | import it.cosenonjaviste.model.MyAdapterFactory; 23 | import it.cosenonjaviste.model.Post; 24 | import it.cosenonjaviste.model.TwitterService; 25 | import it.cosenonjaviste.model.WordPressService; 26 | import okhttp3.OkHttpClient; 27 | import okhttp3.Request; 28 | import retrofit2.Retrofit; 29 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; 30 | import retrofit2.converter.gson.GsonConverterFactory; 31 | 32 | @Module 33 | public class AppModule { 34 | 35 | private Application application; 36 | 37 | public AppModule(Application application) { 38 | this.application = application; 39 | } 40 | 41 | @Provides @Singleton public Gson provideGson() { 42 | return new GsonBuilder() 43 | .setDateFormat("yyyy-MM-dd HH:mm:ss") 44 | .registerTypeAdapterFactory(MyAdapterFactory.create()) 45 | .create(); 46 | } 47 | 48 | @Provides @Singleton public WordPressService provideWordPressService(Gson gson) { 49 | Retrofit retrofit = new Retrofit.Builder() 50 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 51 | .addConverterFactory(new DenvelopingConverter(gson)) 52 | .addConverterFactory(GsonConverterFactory.create(gson)) 53 | .baseUrl("http://www.codingjam.it/") 54 | .build(); 55 | 56 | return retrofit.create(WordPressService.class); 57 | } 58 | 59 | @Provides @Singleton public MailJetService provideMailJetService(Gson gson) { 60 | OkHttpClient.Builder httpClient = new OkHttpClient.Builder(); 61 | 62 | httpClient.addNetworkInterceptor(chain -> { 63 | String userName = BuildConfig.MAILJET_USERNAME; 64 | String password = BuildConfig.MAILJET_PASSWORD; 65 | String string = "Basic " + Base64.encodeToString((userName + ":" + password).getBytes(), Base64.NO_WRAP); 66 | 67 | Request original = chain.request(); 68 | Request.Builder builder = original.newBuilder(); 69 | builder.header("Authorization", string); 70 | Request request = builder.build(); 71 | 72 | return chain.proceed(request); 73 | }); 74 | 75 | Retrofit retrofit = new Retrofit.Builder() 76 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 77 | .addConverterFactory(GsonConverterFactory.create(gson)) 78 | .baseUrl("https://api.mailjet.com/v3") 79 | .build(); 80 | 81 | return retrofit.create(MailJetService.class); 82 | } 83 | 84 | @Provides @Singleton public TwitterService provideTwitterService() { 85 | return new TwitterService(BuildConfig.CONSUMER_KEY, BuildConfig.CONSUMER_SECRET, BuildConfig.ACCESS_TOKEN, BuildConfig.ACCESS_TOKEN_SECRET); 86 | } 87 | 88 | @Provides public Navigator provideNavigator() { 89 | return new AndroidNavigator(); 90 | } 91 | 92 | @Provides @Singleton 93 | public Store, Integer> postListStore(WordPressService wordPressService) { 94 | return StoreBuilder.>key() 95 | .fetcher(integer -> wordPressService.listPosts(integer).toObservable()) 96 | // .persister(persister) 97 | .open(); 98 | } 99 | 100 | @Provides @Singleton 101 | public Store, Integer> authorListStore(WordPressService wordPressService) { 102 | return StoreBuilder.>key() 103 | .fetcher(integer -> wordPressService.listAuthors().toObservable()) 104 | // .persister(persister) 105 | .open(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/ApplicationComponent.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui; 2 | 3 | import android.support.annotation.VisibleForTesting; 4 | 5 | import com.google.gson.Gson; 6 | 7 | import javax.inject.Singleton; 8 | 9 | import dagger.Component; 10 | import it.cosenonjaviste.ui.author.AuthorListFragment; 11 | import it.cosenonjaviste.ui.category.CategoryListFragment; 12 | import it.cosenonjaviste.ui.contact.ContactFragment; 13 | import it.cosenonjaviste.ui.page.PageFragment; 14 | import it.cosenonjaviste.ui.post.PostListFragment; 15 | import it.cosenonjaviste.ui.twitter.TweetListFragment; 16 | 17 | @Singleton 18 | @Component(modules = {AppModule.class}) 19 | public interface ApplicationComponent { 20 | 21 | @VisibleForTesting Gson gson(); 22 | 23 | void inject(PostListFragment postListFragment); 24 | 25 | void inject(AuthorListFragment authorListFragment); 26 | 27 | void inject(PageFragment pageFragment); 28 | 29 | void inject(TweetListFragment tweetListFragment); 30 | 31 | void inject(ContactFragment contactFragment); 32 | 33 | void inject(CategoryListFragment categoryListFragment); 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/CoseNonJavisteApp.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.support.v4.app.Fragment; 6 | 7 | import com.crashlytics.android.Crashlytics; 8 | import com.squareup.leakcanary.LeakCanary; 9 | 10 | import io.fabric.sdk.android.Fabric; 11 | import it.cosenonjaviste.BuildConfig; 12 | 13 | public class CoseNonJavisteApp extends Application { 14 | 15 | private ApplicationComponent component; 16 | 17 | @Override public void onCreate() { 18 | super.onCreate(); 19 | if (!BuildConfig.DEBUG) { 20 | Fabric.with(this, new Crashlytics()); 21 | } 22 | LeakCanary.install(this); 23 | component = DaggerApplicationComponent.builder() 24 | .appModule(new AppModule(this)) 25 | .build(); 26 | } 27 | 28 | public ApplicationComponent getComponent() { 29 | return component; 30 | } 31 | 32 | public static ApplicationComponent getComponent(Fragment fragment) { 33 | return getComponent(fragment.getActivity()); 34 | } 35 | 36 | public static ApplicationComponent getComponent(Context context) { 37 | return ((CoseNonJavisteApp) context.getApplicationContext()).getComponent(); 38 | } 39 | 40 | public void setComponent(ApplicationComponent component) { 41 | this.component = component; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/MainActivity.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui; 2 | 3 | import android.databinding.DataBindingUtil; 4 | import android.os.Bundle; 5 | import android.support.v4.app.Fragment; 6 | import android.support.v4.view.GravityCompat; 7 | import android.support.v7.app.ActionBar; 8 | import android.support.v7.app.AppCompatActivity; 9 | import android.view.MenuItem; 10 | 11 | import it.cosenonjaviste.R; 12 | import it.cosenonjaviste.databinding.ActivityMainBinding; 13 | import it.cosenonjaviste.ui.author.AuthorListFragment; 14 | import it.cosenonjaviste.ui.category.CategoryListFragment; 15 | import it.cosenonjaviste.ui.contact.ContactFragment; 16 | import it.cosenonjaviste.ui.post.PostListFragment; 17 | import it.cosenonjaviste.ui.twitter.TweetListFragment; 18 | 19 | public class MainActivity extends AppCompatActivity { 20 | private ActivityMainBinding binding; 21 | 22 | @Override 23 | protected void onCreate(Bundle savedInstanceState) { 24 | super.onCreate(savedInstanceState); 25 | 26 | binding = DataBindingUtil.setContentView(this, R.layout.activity_main); 27 | 28 | setSupportActionBar(binding.toolbar); 29 | 30 | ActionBar actionBar = getSupportActionBar(); 31 | if (actionBar != null) { 32 | actionBar.setDisplayHomeAsUpEnabled(true); 33 | actionBar.setHomeAsUpIndicator(R.drawable.ic_menu); 34 | } 35 | 36 | binding.drawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START); 37 | binding.leftDrawerMenu.setNavigationItemSelectedListener(menuItem -> { 38 | selectItem(menuItem.getItemId()); 39 | menuItem.setChecked(true); 40 | return true; 41 | }); 42 | 43 | if (savedInstanceState == null) { 44 | selectItem(R.id.drawer_post); 45 | binding.leftDrawerMenu.getMenu().findItem(R.id.drawer_post).setChecked(true); 46 | } 47 | } 48 | 49 | @Override 50 | public boolean onOptionsItemSelected(MenuItem item) { 51 | switch (item.getItemId()) { 52 | case android.R.id.home: 53 | binding.drawerLayout.openDrawer(GravityCompat.START); 54 | return true; 55 | } 56 | 57 | return super.onOptionsItemSelected(item); 58 | } 59 | 60 | private void selectItem(int menuItemId) { 61 | String tag = "fragment_" + menuItemId; 62 | Fragment fragment = getSupportFragmentManager().findFragmentByTag(tag); 63 | 64 | if (fragment == null) { 65 | fragment = createFragment(menuItemId); 66 | } 67 | 68 | getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, fragment, tag).commit(); 69 | 70 | binding.drawerLayout.closeDrawer(binding.leftDrawerMenu); 71 | } 72 | 73 | private Fragment createFragment(int menuItemId) { 74 | switch (menuItemId) { 75 | case R.id.drawer_categories: 76 | return Fragment.instantiate(this, CategoryListFragment.class.getName()); 77 | case R.id.drawer_authors: 78 | return Fragment.instantiate(this, AuthorListFragment.class.getName()); 79 | case R.id.drawer_twitter: 80 | return Fragment.instantiate(this, TweetListFragment.class.getName()); 81 | case R.id.drawer_contacts: 82 | return Fragment.instantiate(this, ContactFragment.class.getName()); 83 | default: 84 | return Fragment.instantiate(this, PostListFragment.class.getName()); 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/MessageManager.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui; 2 | 3 | import android.app.Activity; 4 | import android.support.design.widget.Snackbar; 5 | 6 | public class MessageManager { 7 | public void showMessage(Activity activity, int message) { 8 | if (activity != null) { 9 | Snackbar.make(activity.findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG).show(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/author/AuthorListFragment.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui.author; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.app.Fragment; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | 10 | import javax.inject.Inject; 11 | import javax.inject.Provider; 12 | 13 | import it.codingjam.lifecyclebinder.LifeCycleBinder; 14 | import it.codingjam.lifecyclebinder.RetainedObjectProvider; 15 | import it.cosenonjaviste.core.author.AuthorListViewModel; 16 | import it.cosenonjaviste.databinding.AuthorCellBinding; 17 | import it.cosenonjaviste.databinding.RecyclerBinding; 18 | import it.cosenonjaviste.ui.CoseNonJavisteApp; 19 | import it.cosenonjaviste.ui.utils.RecyclerBindingBuilder; 20 | 21 | public class AuthorListFragment extends Fragment { 22 | 23 | @RetainedObjectProvider("viewModel") @Inject Provider provider; 24 | 25 | AuthorListViewModel viewModel; 26 | 27 | // @BindLifeCycle @Inject AuthorListViewModel viewModel; 28 | 29 | @Override public void onCreate(Bundle state) { 30 | super.onCreate(state); 31 | CoseNonJavisteApp.getComponent(this).inject(this); 32 | LifeCycleBinder.bind(this); 33 | } 34 | 35 | @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 36 | return new RecyclerBindingBuilder<>(viewModel, RecyclerBinding.inflate(inflater, container, false)) 37 | .gridLayoutManager(2) 38 | .viewHolder(viewGroup -> new AuthorViewHolder(AuthorCellBinding.inflate(inflater, viewGroup, false), viewModel)) 39 | .getRoot(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/author/AuthorViewHolder.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui.author; 2 | 3 | import android.databinding.ObservableField; 4 | 5 | import it.cosenonjaviste.core.author.AuthorListViewModel; 6 | import it.cosenonjaviste.databinding.AuthorCellBinding; 7 | import it.cosenonjaviste.model.Author; 8 | import it.cosenonjaviste.ui.recycler.BindableViewHolder; 9 | 10 | 11 | public class AuthorViewHolder extends BindableViewHolder { 12 | public final ObservableField item = new ObservableField<>(); 13 | 14 | private AuthorCellBinding binding; 15 | 16 | private AuthorListViewModel viewModel; 17 | 18 | public AuthorViewHolder(AuthorCellBinding binding, AuthorListViewModel viewModel) { 19 | super(binding.getRoot()); 20 | this.binding = binding; 21 | this.viewModel = viewModel; 22 | binding.setViewHolder(this); 23 | } 24 | 25 | @Override public void bind(Author item) { 26 | this.item.set(item); 27 | binding.executePendingBindings(); 28 | } 29 | 30 | public void onItemClicked() { 31 | viewModel.goToAuthorDetail(getAdapterPosition()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/bind/DataBindingConverters.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui.bind; 2 | 3 | import android.databinding.BindingAdapter; 4 | import android.databinding.BindingConversion; 5 | import android.support.design.widget.TextInputLayout; 6 | import android.text.Html; 7 | import android.text.TextUtils; 8 | import android.view.View; 9 | import android.webkit.WebView; 10 | import android.widget.ImageView; 11 | import android.widget.TextView; 12 | 13 | import com.squareup.picasso.Picasso; 14 | 15 | import java.util.Date; 16 | 17 | import it.cosenonjaviste.R; 18 | import it.cosenonjaviste.ui.utils.CircleTransform; 19 | import it.cosenonjaviste.ui.utils.DateFormatter; 20 | 21 | public class DataBindingConverters { 22 | 23 | private static CircleTransform circleTransformation; 24 | 25 | @BindingConversion 26 | public static CharSequence convertDateToCharSequence(Date date) { 27 | return DateFormatter.formatDate(date); 28 | } 29 | 30 | @BindingAdapter("error") 31 | public static void bindValidationError(TextInputLayout textInputLayout, int errorRes) { 32 | if (errorRes != 0) { 33 | textInputLayout.setError(textInputLayout.getResources().getString(errorRes)); 34 | } else { 35 | textInputLayout.setError(null); 36 | } 37 | } 38 | 39 | @BindingAdapter("visibleOrGone") 40 | public static void bindVisibleOrGone(View view, boolean b) { 41 | view.setVisibility(b ? View.VISIBLE : View.GONE); 42 | } 43 | 44 | @BindingAdapter("visible") 45 | public static void bindVisible(View view, boolean b) { 46 | view.setVisibility(b ? View.VISIBLE : View.INVISIBLE); 47 | } 48 | 49 | @BindingAdapter("userImageUrl") 50 | public static void loadUserImage(ImageView view, String url) { 51 | if (!TextUtils.isEmpty(url)) { 52 | if (circleTransformation == null) { 53 | circleTransformation = CircleTransform.createWithBorder(view.getResources(), R.dimen.author_image_size_big, R.color.colorPrimary, R.dimen.author_image_border); 54 | } 55 | Picasso.with(view.getContext()).load(url).transform(circleTransformation).into(view); 56 | } else { 57 | view.setImageDrawable(null); 58 | } 59 | } 60 | 61 | @BindingAdapter("imageUrl") 62 | public static void loadImage(ImageView view, String url) { 63 | if (!TextUtils.isEmpty(url)) { 64 | Picasso.with(view.getContext()).load(url).into(view); 65 | } else { 66 | view.setImageDrawable(null); 67 | } 68 | } 69 | 70 | @BindingAdapter("textHtml") 71 | public static void bindHtmlText(TextView view, String text) { 72 | if (text != null) { 73 | view.setText(Html.fromHtml(text)); 74 | } else { 75 | view.setText(""); 76 | } 77 | } 78 | 79 | @BindingAdapter("android:onClick") 80 | public static void bindOnClick(View view, Runnable listener) { 81 | view.setOnClickListener(v -> listener.run()); 82 | } 83 | 84 | @BindingAdapter("url") 85 | public static void bindOnClick(WebView view, String url) { 86 | view.loadUrl(url); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/category/CategoryListFragment.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui.category; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.app.Fragment; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | 10 | import javax.inject.Inject; 11 | import javax.inject.Provider; 12 | 13 | import it.codingjam.lifecyclebinder.LifeCycleBinder; 14 | import it.codingjam.lifecyclebinder.RetainedObjectProvider; 15 | import it.cosenonjaviste.core.category.CategoryListViewModel; 16 | import it.cosenonjaviste.databinding.CategoryRowBinding; 17 | import it.cosenonjaviste.databinding.RecyclerBinding; 18 | import it.cosenonjaviste.ui.CoseNonJavisteApp; 19 | import it.cosenonjaviste.ui.utils.RecyclerBindingBuilder; 20 | 21 | public class CategoryListFragment extends Fragment { 22 | 23 | @RetainedObjectProvider("viewModel") @Inject Provider provider; 24 | 25 | CategoryListViewModel viewModel; 26 | 27 | @Override public void onCreate(Bundle state) { 28 | super.onCreate(state); 29 | CoseNonJavisteApp.getComponent(this).inject(this); 30 | LifeCycleBinder.bind(this); 31 | } 32 | 33 | @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 34 | return new RecyclerBindingBuilder<>(viewModel, RecyclerBinding.inflate(inflater, container, false)) 35 | .gridLayoutManager(2) 36 | .viewHolder(viewGroup -> new CategoryViewHolder(CategoryRowBinding.inflate(inflater, viewGroup, false), viewModel)) 37 | .getRoot(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/category/CategoryViewHolder.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui.category; 2 | 3 | import android.databinding.ObservableField; 4 | 5 | import it.cosenonjaviste.core.category.CategoryListViewModel; 6 | import it.cosenonjaviste.databinding.CategoryRowBinding; 7 | import it.cosenonjaviste.model.Category; 8 | import it.cosenonjaviste.ui.recycler.BindableViewHolder; 9 | 10 | 11 | public class CategoryViewHolder extends BindableViewHolder { 12 | public final ObservableField item = new ObservableField<>(); 13 | 14 | private CategoryRowBinding binding; 15 | 16 | private CategoryListViewModel viewModel; 17 | 18 | public CategoryViewHolder(CategoryRowBinding binding, CategoryListViewModel viewModel) { 19 | super(binding.getRoot()); 20 | this.binding = binding; 21 | this.viewModel = viewModel; 22 | binding.setViewHolder(this); 23 | } 24 | 25 | @Override public void bind(Category item) { 26 | this.item.set(item); 27 | binding.executePendingBindings(); 28 | } 29 | 30 | public void onItemClicked() { 31 | viewModel.goToPosts(getAdapterPosition()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/contact/ContactFragment.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui.contact; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.app.Fragment; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | 10 | import javax.inject.Inject; 11 | import javax.inject.Provider; 12 | 13 | import it.codingjam.lifecyclebinder.LifeCycleBinder; 14 | import it.codingjam.lifecyclebinder.RetainedObjectProvider; 15 | import it.cosenonjaviste.R; 16 | import it.cosenonjaviste.core.contact.ContactViewModel; 17 | import it.cosenonjaviste.databinding.ContactBinding; 18 | import it.cosenonjaviste.ui.CoseNonJavisteApp; 19 | 20 | public class ContactFragment extends Fragment { 21 | 22 | @RetainedObjectProvider("viewModel") @Inject Provider provider; 23 | 24 | ContactViewModel viewModel; 25 | 26 | @Override public void onCreate(Bundle state) { 27 | super.onCreate(state); 28 | CoseNonJavisteApp.getComponent(this).inject(this); 29 | LifeCycleBinder.bind(this); 30 | } 31 | 32 | @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 33 | ContactBinding binding = ContactBinding.bind(inflater.inflate(R.layout.contact, container, false)); 34 | binding.setViewModel(viewModel); 35 | return binding.getRoot(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/post/PostListFragment.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui.post; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.app.Fragment; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | 11 | import javax.inject.Inject; 12 | import javax.inject.Provider; 13 | 14 | import it.codingjam.lifecyclebinder.LifeCycleBinder; 15 | import it.codingjam.lifecyclebinder.RetainedObjectProvider; 16 | import it.cosenonjaviste.core.post.PostListViewModel; 17 | import it.cosenonjaviste.databinding.PostRowBinding; 18 | import it.cosenonjaviste.databinding.RecyclerBinding; 19 | import it.cosenonjaviste.ui.CoseNonJavisteApp; 20 | import it.cosenonjaviste.ui.utils.RecyclerBindingBuilder; 21 | 22 | public class PostListFragment extends Fragment { 23 | 24 | @RetainedObjectProvider("viewModel") @Inject Provider provider; 25 | 26 | PostListViewModel viewModel; 27 | 28 | @Override public void onCreate(Bundle state) { 29 | super.onCreate(state); 30 | CoseNonJavisteApp.getComponent(this).inject(this); 31 | LifeCycleBinder.bind(this); 32 | } 33 | 34 | @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 35 | RecyclerBinding binding = RecyclerBinding.inflate(inflater, container, false); 36 | AppCompatActivity activity = (AppCompatActivity) getActivity(); 37 | 38 | if (viewModel.isToolbarVisible()) { 39 | binding.toolbar.setVisibility(View.VISIBLE); 40 | activity.setSupportActionBar(binding.toolbar); 41 | if (activity.getSupportActionBar() != null) { 42 | activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true); 43 | activity.getSupportActionBar().setTitle(viewModel.getToolbarTitle()); 44 | } 45 | } 46 | 47 | return new RecyclerBindingBuilder<>(viewModel, binding) 48 | .viewHolder(viewGroup -> new PostViewHolder(PostRowBinding.inflate(inflater, viewGroup, false), viewModel)) 49 | .loadMoreListener(viewModel::loadNextPage) 50 | .getRoot(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/post/PostViewHolder.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui.post; 2 | 3 | import android.databinding.ObservableField; 4 | 5 | import it.cosenonjaviste.core.post.PostListViewModel; 6 | import it.cosenonjaviste.databinding.PostRowBinding; 7 | import it.cosenonjaviste.model.Post; 8 | import it.cosenonjaviste.ui.recycler.BindableViewHolder; 9 | 10 | 11 | public class PostViewHolder extends BindableViewHolder { 12 | public final ObservableField item = new ObservableField<>(); 13 | 14 | private PostRowBinding binding; 15 | 16 | private PostListViewModel viewModel; 17 | 18 | public PostViewHolder(PostRowBinding binding, PostListViewModel viewModel) { 19 | super(binding.getRoot()); 20 | this.binding = binding; 21 | this.viewModel = viewModel; 22 | binding.setViewHolder(this); 23 | } 24 | 25 | @Override public void bind(Post item) { 26 | this.item.set(item); 27 | binding.executePendingBindings(); 28 | } 29 | 30 | public void onItemClicked() { 31 | viewModel.goToDetail(getAdapterPosition()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/recycler/AdapterOnListChangedCallback.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Fabio Collini. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package it.cosenonjaviste.ui.recycler; 17 | 18 | import android.databinding.ObservableList; 19 | import android.support.v7.widget.RecyclerView; 20 | 21 | public class AdapterOnListChangedCallback extends ObservableList.OnListChangedCallback> { 22 | 23 | private RecyclerView.Adapter adapter; 24 | 25 | public AdapterOnListChangedCallback(RecyclerView.Adapter adapter) { 26 | this.adapter = adapter; 27 | } 28 | 29 | @Override public void onChanged(ObservableList sender) { 30 | adapter.notifyDataSetChanged(); 31 | } 32 | 33 | @Override public void onItemRangeChanged(ObservableList sender, int positionStart, int itemCount) { 34 | adapter.notifyItemRangeChanged(positionStart, itemCount); 35 | } 36 | 37 | @Override public void onItemRangeInserted(ObservableList sender, int positionStart, int itemCount) { 38 | adapter.notifyItemRangeInserted(positionStart, itemCount); 39 | } 40 | 41 | @Override public void onItemRangeMoved(ObservableList sender, int fromPosition, int toPosition, int itemCount) { 42 | adapter.notifyDataSetChanged(); 43 | } 44 | 45 | @Override public void onItemRangeRemoved(ObservableList sender, int positionStart, int itemCount) { 46 | adapter.notifyItemRangeRemoved(positionStart, itemCount); 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/recycler/BindableAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Fabio Collini. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package it.cosenonjaviste.ui.recycler; 17 | 18 | import android.databinding.ObservableList; 19 | import android.support.v7.widget.RecyclerView; 20 | import android.view.ViewGroup; 21 | 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | 25 | public class BindableAdapter extends RecyclerView.Adapter> { 26 | 27 | private final ObservableList.OnListChangedCallback> onListChangedCallback; 28 | 29 | private ObservableList items; 30 | 31 | private List> viewHolderFactories = new ArrayList<>(); 32 | 33 | private ViewHolderFactory defaultViewHolderFactory; 34 | 35 | private List viewTypeSelectors = new ArrayList<>(); 36 | 37 | private BindListener onBindListener; 38 | 39 | public BindableAdapter(ObservableList items) { 40 | this(items, null); 41 | } 42 | 43 | public BindableAdapter(ObservableList items, ViewHolderFactory defaultViewHolderFactory) { 44 | this.items = items; 45 | this.defaultViewHolderFactory = defaultViewHolderFactory; 46 | //saved in a field to maintain a reference and avoid garbage collection 47 | onListChangedCallback = new AdapterOnListChangedCallback<>(this); 48 | items.addOnListChangedCallback((ObservableList.OnListChangedCallback) new WeakOnListChangedCallback<>(onListChangedCallback)); 49 | if (!items.isEmpty()) { 50 | notifyDataSetChanged(); 51 | } 52 | } 53 | 54 | public void addViewType(ViewHolderFactory viewHolderFactory, ViewTypeSelector selector) { 55 | viewHolderFactories.add(viewHolderFactory); 56 | viewTypeSelectors.add(selector); 57 | } 58 | 59 | public void addViewType(ViewHolderFactory viewHolderFactory, final Class itemClass) { 60 | viewHolderFactories.add(viewHolderFactory); 61 | viewTypeSelectors.add(new ClassViewTypeSelector<>(itemClass)); 62 | } 63 | 64 | @Override public int getItemViewType(int position) { 65 | int i = 0; 66 | for (ViewTypeSelector selector : viewTypeSelectors) { 67 | if (selector.isOfViewType(position)) { 68 | return i; 69 | } 70 | i++; 71 | } 72 | if (defaultViewHolderFactory == null) { 73 | throw new RuntimeException("No factory found and no default factory available for item in position " + position); 74 | } 75 | return -1; 76 | } 77 | 78 | @Override public BindableViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { 79 | ViewHolderFactory factory; 80 | if (viewType != -1) { 81 | factory = viewHolderFactories.get(viewType); 82 | } else { 83 | factory = defaultViewHolderFactory; 84 | } 85 | return (BindableViewHolder) factory.create(viewGroup); 86 | } 87 | 88 | @Override public void onBindViewHolder(BindableViewHolder viewHolder, int position) { 89 | viewHolder.bind(items.get(position)); 90 | if (onBindListener != null) { 91 | onBindListener.call(viewHolder, position); 92 | } 93 | } 94 | 95 | @Override public int getItemCount() { 96 | return items.size(); 97 | } 98 | 99 | public void setOnBindListener(BindListener onBindListener) { 100 | this.onBindListener = onBindListener; 101 | } 102 | 103 | public interface ViewHolderFactory { 104 | BindableViewHolder create(ViewGroup viewGroup); 105 | } 106 | 107 | public interface ViewTypeSelector { 108 | boolean isOfViewType(int position); 109 | } 110 | 111 | public interface BindListener { 112 | void call(BindableViewHolder viewHolder, Integer position); 113 | } 114 | 115 | private class ClassViewTypeSelector implements ViewTypeSelector { 116 | private final Class itemClass; 117 | 118 | public ClassViewTypeSelector(Class itemClass) { 119 | this.itemClass = itemClass; 120 | } 121 | 122 | @Override public boolean isOfViewType(int position) { 123 | T item = items.get(position); 124 | return item != null && item.getClass().isAssignableFrom(itemClass); 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/recycler/BindableViewHolder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Fabio Collini. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package it.cosenonjaviste.ui.recycler; 17 | 18 | import android.support.v7.widget.RecyclerView; 19 | import android.view.View; 20 | 21 | public abstract class BindableViewHolder extends RecyclerView.ViewHolder { 22 | 23 | public BindableViewHolder(View itemView) { 24 | super(itemView); 25 | } 26 | 27 | public abstract void bind(T item); 28 | } -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/recycler/WeakOnListChangedCallback.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Fabio Collini. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package it.cosenonjaviste.ui.recycler; 17 | 18 | import android.databinding.ObservableList; 19 | import android.databinding.ObservableList.OnListChangedCallback; 20 | 21 | import java.lang.ref.WeakReference; 22 | 23 | public class WeakOnListChangedCallback extends OnListChangedCallback { 24 | 25 | private WeakReference> ref; 26 | 27 | public WeakOnListChangedCallback(OnListChangedCallback delegate) { 28 | this.ref = new WeakReference<>(delegate); 29 | } 30 | 31 | @Override public void onChanged(T sender) { 32 | if (ref.get() != null) { 33 | ref.get().onChanged(sender); 34 | } 35 | } 36 | 37 | @Override public void onItemRangeChanged(T sender, int positionStart, int itemCount) { 38 | if (ref.get() != null) { 39 | ref.get().onItemRangeChanged(sender, positionStart, itemCount); 40 | } 41 | } 42 | 43 | @Override public void onItemRangeInserted(T sender, int positionStart, int itemCount) { 44 | if (ref.get() != null) { 45 | ref.get().onItemRangeInserted(sender, positionStart, itemCount); 46 | } 47 | } 48 | 49 | @Override public void onItemRangeMoved(T sender, int fromPosition, int toPosition, int itemCount) { 50 | if (ref.get() != null) { 51 | ref.get().onItemRangeMoved(sender, fromPosition, toPosition, itemCount); 52 | } 53 | } 54 | 55 | @Override public void onItemRangeRemoved(T sender, int positionStart, int itemCount) { 56 | if (ref.get() != null) { 57 | ref.get().onItemRangeRemoved(sender, positionStart, itemCount); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/twitter/TweetListFragment.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui.twitter; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.app.Fragment; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | 10 | import javax.inject.Inject; 11 | import javax.inject.Provider; 12 | 13 | import it.codingjam.lifecyclebinder.LifeCycleBinder; 14 | import it.codingjam.lifecyclebinder.RetainedObjectProvider; 15 | import it.cosenonjaviste.core.twitter.TweetListViewModel; 16 | import it.cosenonjaviste.databinding.RecyclerBinding; 17 | import it.cosenonjaviste.databinding.TweetRowBinding; 18 | import it.cosenonjaviste.ui.CoseNonJavisteApp; 19 | import it.cosenonjaviste.ui.utils.RecyclerBindingBuilder; 20 | 21 | public class TweetListFragment extends Fragment { 22 | 23 | @RetainedObjectProvider("viewModel") @Inject Provider provider; 24 | 25 | TweetListViewModel viewModel; 26 | 27 | @Override public void onCreate(Bundle state) { 28 | super.onCreate(state); 29 | CoseNonJavisteApp.getComponent(this).inject(this); 30 | LifeCycleBinder.bind(this); 31 | } 32 | 33 | @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 34 | return new RecyclerBindingBuilder<>(viewModel, RecyclerBinding.inflate(inflater, container, false)) 35 | .viewHolder(viewGroup -> new TweetViewHolder(TweetRowBinding.inflate(inflater, viewGroup, false))) 36 | .loadMoreListener(viewModel::loadNextPage) 37 | .getRoot(); 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/twitter/TweetViewHolder.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui.twitter; 2 | 3 | import android.databinding.ObservableField; 4 | 5 | import it.cosenonjaviste.databinding.TweetRowBinding; 6 | import it.cosenonjaviste.model.Tweet; 7 | import it.cosenonjaviste.ui.recycler.BindableViewHolder; 8 | 9 | 10 | public class TweetViewHolder extends BindableViewHolder { 11 | public final ObservableField item = new ObservableField<>(); 12 | 13 | private TweetRowBinding binding; 14 | 15 | public TweetViewHolder(TweetRowBinding binding) { 16 | super(binding.getRoot()); 17 | this.binding = binding; 18 | binding.setViewHolder(this); 19 | } 20 | 21 | @Override public void bind(Tweet item) { 22 | this.item.set(item); 23 | binding.executePendingBindings(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/utils/CircleTransform.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui.utils; 2 | 3 | import android.content.res.Resources; 4 | import android.graphics.Bitmap; 5 | import android.graphics.Bitmap.Config; 6 | import android.graphics.BitmapShader; 7 | import android.graphics.Canvas; 8 | import android.graphics.Paint; 9 | 10 | import com.squareup.picasso.Transformation; 11 | 12 | public class CircleTransform implements Transformation { 13 | 14 | private final boolean showBorder; 15 | private int borderSize; 16 | private int borderColor; 17 | private int imageSize; 18 | 19 | public static CircleTransform createWithBorder(Resources resources, int dimensionResource, int colorRes, int borderSizeResource) { 20 | return new CircleTransform(resources, dimensionResource, true, colorRes, borderSizeResource); 21 | } 22 | 23 | private CircleTransform(Resources resources, int dimensionResource, boolean showBorder, int colorRes, int borderSizeResource) { 24 | this.showBorder = showBorder; 25 | if (borderSizeResource != 0) { 26 | borderSize = resources.getDimensionPixelSize(borderSizeResource); 27 | } 28 | imageSize = resources.getDimensionPixelSize(dimensionResource); 29 | if (colorRes != 0) { 30 | borderColor = resources.getColor(colorRes); 31 | } 32 | } 33 | 34 | @Override 35 | public Bitmap transform(Bitmap source) { 36 | if (source == null) { 37 | return null; 38 | } 39 | int size = Math.min(source.getWidth(), source.getHeight()); 40 | Bitmap squaredBitmap = createSquaredImage(source, size); 41 | 42 | try { 43 | if (size != imageSize) { 44 | size = imageSize; 45 | squaredBitmap = screateScaledImage(squaredBitmap, size); 46 | } 47 | 48 | Bitmap bitmap = Bitmap.createBitmap(size, size, Config.ARGB_8888); 49 | 50 | Canvas canvas = new Canvas(bitmap); 51 | 52 | float r = size / 2f; 53 | if (showBorder) { 54 | Paint borderPaint = new Paint(); 55 | borderPaint.setStrokeWidth(borderSize); 56 | borderPaint.setColor(borderColor); 57 | borderPaint.setAntiAlias(true); 58 | 59 | float borderRadius = size / 2f; 60 | canvas.drawCircle(borderRadius, borderRadius, borderRadius, borderPaint); 61 | 62 | r = r - borderSize; 63 | } 64 | 65 | canvas.drawCircle(size / 2f, size / 2f, r, createBitmapPainter(squaredBitmap)); 66 | 67 | squaredBitmap.recycle(); 68 | return bitmap; 69 | } catch (IllegalArgumentException e) { 70 | return squaredBitmap; 71 | } 72 | } 73 | 74 | private Paint createBitmapPainter(Bitmap squaredBitmap) { 75 | Paint paint = new Paint(); 76 | BitmapShader shader = new BitmapShader(squaredBitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP); 77 | paint.setShader(shader); 78 | paint.setAntiAlias(true); 79 | return paint; 80 | } 81 | 82 | private Bitmap screateScaledImage(Bitmap source, int size) { 83 | Bitmap ret = Bitmap.createScaledBitmap(source, size, size, true); 84 | if (source != ret) { 85 | source.recycle(); 86 | } 87 | return ret; 88 | } 89 | 90 | private Bitmap createSquaredImage(Bitmap source, int size) { 91 | int width = source.getWidth(); 92 | int height = source.getHeight(); 93 | if (width == size && height == size) { 94 | return source; 95 | } 96 | int x = Math.max(0, (width - size) / 2); 97 | int y = Math.max(0, (height - size) / 2); 98 | 99 | Bitmap squaredBitmap = Bitmap.createBitmap(source, x, y, size, size); 100 | if (squaredBitmap != source) { 101 | source.recycle(); 102 | } 103 | return squaredBitmap; 104 | } 105 | 106 | @Override 107 | public String key() { 108 | return "circle" + imageSize + "_" + borderColor + "_" + showBorder; 109 | } 110 | } -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/utils/DateFormatter.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui.utils; 2 | 3 | import android.text.format.DateUtils; 4 | 5 | import java.util.Date; 6 | 7 | public class DateFormatter { 8 | public static CharSequence formatDate(Date date) { 9 | return DateUtils.getRelativeTimeSpanString(date.getTime(), System.currentTimeMillis(), DateUtils.DAY_IN_MILLIS, DateUtils.FORMAT_SHOW_YEAR); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/utils/EndlessRecyclerOnScrollListener.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui.utils; 2 | 3 | import android.support.v7.widget.LinearLayoutManager; 4 | import android.support.v7.widget.RecyclerView; 5 | 6 | public class EndlessRecyclerOnScrollListener extends RecyclerView.OnScrollListener { 7 | 8 | private int previousTotal = 0; // The total number of items in the dataset after the last load 9 | private boolean loading = true; // True if we are still waiting for the last set of data to load. 10 | private int visibleThreshold = 2; // The minimum amount of items to have below your current scroll position before loading more. 11 | private int firstVisibleItem, visibleItemCount, totalItemCount; 12 | 13 | private Runnable listener; 14 | 15 | public EndlessRecyclerOnScrollListener(Runnable listener) { 16 | this.listener = listener; 17 | } 18 | 19 | @Override 20 | public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 21 | super.onScrolled(recyclerView, dx, dy); 22 | LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); 23 | 24 | visibleItemCount = recyclerView.getChildCount(); 25 | totalItemCount = layoutManager.getItemCount(); 26 | firstVisibleItem = layoutManager.findFirstVisibleItemPosition(); 27 | 28 | if (loading) { 29 | if (totalItemCount > previousTotal) { 30 | loading = false; 31 | previousTotal = totalItemCount; 32 | } 33 | } 34 | if (!loading && (totalItemCount - visibleItemCount) 35 | <= (firstVisibleItem + visibleThreshold)) { 36 | // End has been reached 37 | 38 | // Do something 39 | onLoadMore(); 40 | 41 | loading = true; 42 | } 43 | } 44 | 45 | public void onLoadMore() { 46 | listener.run(); 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/utils/RecyclerBindingBuilder.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui.utils; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.v7.widget.GridLayoutManager; 5 | import android.support.v7.widget.LinearLayoutManager; 6 | import android.view.View; 7 | 8 | import it.cosenonjaviste.R; 9 | import it.cosenonjaviste.core.list.ListModel; 10 | import it.cosenonjaviste.core.list.RxListViewModel; 11 | import it.cosenonjaviste.databinding.RecyclerBinding; 12 | import it.cosenonjaviste.ui.recycler.BindableAdapter; 13 | 14 | public class RecyclerBindingBuilder { 15 | 16 | private final RxListViewModel> viewModel; 17 | 18 | private RecyclerBinding binding; 19 | 20 | public RecyclerBindingBuilder(RxListViewModel> viewModel, RecyclerBinding binding) { 21 | this.viewModel = viewModel; 22 | this.binding = binding; 23 | this.binding.swipeRefresh.setColorSchemeResources(R.color.colorPrimary, R.color.cnj_border, R.color.cnj_selection); 24 | this.binding.setViewModel(viewModel); 25 | } 26 | 27 | public RecyclerBinding getBinding() { 28 | if (binding.list.getLayoutManager() == null) { 29 | linearLayoutManager(); 30 | } 31 | return binding; 32 | } 33 | 34 | public View getRoot() { 35 | return getBinding().getRoot(); 36 | } 37 | 38 | public RecyclerBindingBuilder linearLayoutManager() { 39 | binding.list.setLayoutManager(new LinearLayoutManager(binding.list.getContext())); 40 | return this; 41 | } 42 | 43 | public RecyclerBindingBuilder gridLayoutManager(int spanCount) { 44 | binding.list.setLayoutManager(new GridLayoutManager(binding.list.getContext(), spanCount)); 45 | return this; 46 | } 47 | 48 | public RecyclerBindingBuilder loadMoreListener(Runnable listener) { 49 | binding.list.addOnScrollListener(new EndlessRecyclerOnScrollListener(listener)); 50 | return this; 51 | } 52 | 53 | @NonNull 54 | public RecyclerBindingBuilder viewHolder(BindableAdapter.ViewHolderFactory factory) { 55 | binding.list.setAdapter(new BindableAdapter<>(viewModel.getModel().getItems(), factory)); 56 | return this; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/utils/SingleFragmentActivity.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui.utils; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.os.Parcelable; 7 | import android.support.v4.app.Fragment; 8 | import android.support.v4.app.FragmentActivity; 9 | import android.support.v7.app.AppCompatActivity; 10 | import android.view.MenuItem; 11 | 12 | import it.cosenonjaviste.R; 13 | import it.cosenonjaviste.core.base.ArgumentManager; 14 | 15 | public class SingleFragmentActivity extends AppCompatActivity { 16 | 17 | private static final String VIEW_CLASS = "viewClass"; 18 | 19 | public static Intent populateIntent(Intent intent, Class viewClass) { 20 | intent.putExtra(VIEW_CLASS, viewClass.getName()); 21 | return intent; 22 | } 23 | 24 | public static Intent createIntent(Context context, Class viewClass) { 25 | Intent intent = new Intent(context, SingleFragmentActivity.class); 26 | populateIntent(intent, viewClass); 27 | return intent; 28 | } 29 | 30 | public static Intent createIntent(Class viewClass) { 31 | Intent intent = new Intent(); 32 | populateIntent(intent, viewClass); 33 | return intent; 34 | } 35 | 36 | public static void open(FragmentActivity activity, Class viewClass, ARG arg) { 37 | Intent intent = createIntent(activity, viewClass); 38 | ArgumentManager.writeArgument(intent, arg); 39 | activity.startActivity(intent); 40 | } 41 | 42 | @Override protected void onCreate(Bundle savedInstanceState) { 43 | super.onCreate(savedInstanceState); 44 | 45 | setContentView(R.layout.single_fragment); 46 | 47 | if (savedInstanceState == null) { 48 | String viewClassName = getIntent().getStringExtra(VIEW_CLASS); 49 | Fragment fragment = Fragment.instantiate(this, viewClassName); 50 | Bundle extras = getIntent().getExtras(); 51 | if (extras != null) { 52 | Bundle b = new Bundle(extras); 53 | fragment.setArguments(b); 54 | } 55 | getSupportFragmentManager().beginTransaction().add(R.id.single_fragment_root, fragment).commit(); 56 | } 57 | } 58 | 59 | @Override public boolean onOptionsItemSelected(MenuItem item) { 60 | if (item.getItemId() == android.R.id.home) { 61 | finish(); 62 | return true; 63 | } 64 | return super.onOptionsItemSelected(item); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/it/cosenonjaviste/ui/utils/TextWatcherAdapter.java: -------------------------------------------------------------------------------- 1 | package it.cosenonjaviste.ui.utils; 2 | 3 | import android.text.Editable; 4 | import android.text.TextWatcher; 5 | 6 | public class TextWatcherAdapter implements TextWatcher { 7 | @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { 8 | } 9 | 10 | @Override public void onTextChanged(CharSequence s, int start, int before, int count) { 11 | } 12 | 13 | @Override public void afterTextChanged(Editable s) { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/project.properties: -------------------------------------------------------------------------------- 1 | android.library.reference.1=../../build/intermediates/exploded-aar/com.android.support/appcompat-v7/21.0.0 2 | android.library.reference.2=../../build/intermediates/exploded-aar/com.android.support/support-v4/21.0.0 3 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/drawer_shadow.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-hdpi/drawer_shadow.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_email_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-hdpi/ic_email_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_home_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-hdpi/ic_home_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_label_outline_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-hdpi/ic_label_outline_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-hdpi/ic_menu.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_person_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-hdpi/ic_person_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_public_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-hdpi/ic_public_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_share_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-hdpi/ic_share_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/progress_in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-hdpi/progress_in.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/progress_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-hdpi/progress_out.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/drawer_shadow.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-mdpi/drawer_shadow.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_email_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-mdpi/ic_email_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_home_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-mdpi/ic_home_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_label_outline_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-mdpi/ic_label_outline_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-mdpi/ic_menu.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_person_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-mdpi/ic_person_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_public_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-mdpi/ic_public_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_share_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-mdpi/ic_share_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/progress_in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-mdpi/progress_in.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/progress_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-mdpi/progress_out.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/drawer_shadow.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xhdpi/drawer_shadow.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_email_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xhdpi/ic_email_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_home_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xhdpi/ic_home_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_label_outline_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xhdpi/ic_label_outline_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xhdpi/ic_menu.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_person_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xhdpi/ic_person_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_public_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xhdpi/ic_public_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_share_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xhdpi/ic_share_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/progress_in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xhdpi/progress_in.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/progress_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xhdpi/progress_out.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_email_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xxhdpi/ic_email_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_home_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xxhdpi/ic_home_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_label_outline_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xxhdpi/ic_label_outline_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xxhdpi/ic_menu.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_person_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xxhdpi/ic_person_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_public_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xxhdpi/ic_public_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_share_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xxhdpi/ic_share_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/progress_in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xxhdpi/progress_in.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/progress_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xxhdpi/progress_out.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_email_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xxxhdpi/ic_email_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_home_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xxxhdpi/ic_home_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_label_outline_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xxxhdpi/ic_label_outline_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xxxhdpi/ic_menu.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_person_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xxxhdpi/ic_person_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_public_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xxxhdpi/ic_public_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_share_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable-xxxhdpi/ic_share_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/detail_shadow.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/drawer_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/logocnj.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-jam/CodingJamAndroidApp/8e79b4e19e73094f2ebe10ccd92873f7a8fa4660/app/src/main/res/drawable/logocnj.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/progress_bar.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 14 | 15 | 21 | 22 | 27 | 28 | 29 | 30 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/author_cell.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 19 | 20 | 26 | 27 | 32 | 33 | 42 | 43 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/layout/category_row.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 18 | 19 | 24 | 25 | 33 | 34 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/layout/contact.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 15 | 16 | 21 | 22 | 26 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 43 | 44 | 51 | 52 | 53 | 57 | 58 | 65 | 66 | 67 | 71 | 72 |