diff --git a/README.txt b/README.txt new file mode 100755 index 0000000..fccefcf --- /dev/null +++ b/README.txt @@ -0,0 +1,30 @@ +This project implements AndroidClean architecture(http://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/). +The objective is the separation of concers and facilitating testing. The task is to fetch movie events in Finnkino and display them in the app. + +There are three modules: + +1.domain: A Java module without Android dependencies. +-Application business rules +-All use cases(interactors) +-JUnit plus mockito for unit tests + +2.data: An Android module from where all the data is retrieved. Finnkino Api is chosen in this module(http://www.finnkino.fi/XML) +-Deliver data needed by app +-JUnit plus mockito for unit tests + +3.app: An android module that represents the presentation layer. +-Presentation Layer +-Espresso for functional testing. +Note: NowShowingInTheatresFragment is implemented in MVP fashion + +Libraries used in the project: +Dagger2 - Facilitate dependency injections and testing. +Rxjava - Faciliitate async operations and apply reactive programming +Picasso - Image download and display, there are some alternatives such as Glide and Fresco. Picasso is the easiest one to use. +Retrofit - HTTP client +LeakCanary - Memory leak detection +Retrolambda - Support Lambda Expressions +Note: please install jdk 8 before building + +Mosby - Android MVP library. The intention of using this library is to facilicate implemnting Loading-Content-Error in Fragment. +Usually a Fragment is doing the same thing over and over again. It loads data in background, display a loading view (i.e ProgressBar) while loading, displays the loaded data on screen or displays an error view if loading failed. \ No newline at end of file diff --git a/app-debug.apk b/app-debug.apk new file mode 100755 index 0000000..0ec03ff Binary files /dev/null and b/app-debug.apk differ diff --git a/app/.gitignore b/app/.gitignore old mode 100644 new mode 100755 diff --git a/app/build.gradle b/app/build.gradle old mode 100644 new mode 100755 index 350d0ab..08ab36b --- a/app/build.gradle +++ b/app/build.gradle @@ -1,15 +1,30 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'me.tatarka:gradle-retrolambda:3.2.3' + classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4' + //classpath 'com.jakewharton.hugo:hugo-plugin:1.2.1' + } +} + +apply plugin: 'me.tatarka.retrolambda' apply plugin: 'com.android.application' +apply plugin: 'android-apt' android { - compileSdkVersion 22 - buildToolsVersion "22.0.1" + compileSdkVersion rootProject.ext.android.compileSdkVersion + buildToolsVersion rootProject.ext.android.buildToolsVersion defaultConfig { applicationId "android.coding.interview.makeitawesome" - minSdkVersion 14 - targetSdkVersion 22 + minSdkVersion rootProject.ext.android.minSdkVersion + targetSdkVersion rootProject.ext.android.targetSdkVersion versionCode 1 versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { @@ -17,11 +32,41 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + + compileOptions { + encoding "UTF-8" + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:22.2.0' - compile 'com.android.support:design:22.2.0' - compile 'com.android.support:recyclerview-v7:22.2.0' + compile project(':data') + compile rootProject.ext.dependencies.supportAppCompat + compile rootProject.ext.dependencies.supportDesign + compile rootProject.ext.dependencies.supportRecyclerview + compile rootProject.ext.dependencies.supportAnnotations + compile rootProject.ext.dependencies.rxandroid + compile rootProject.ext.dependencies.mosby + compile rootProject.ext.dependencies.mosbyViewState + compile rootProject.ext.dependencies.dagger2 + compile rootProject.ext.dependencies.picasso + + debugCompile rootProject.ext.dependencies.leakcanary + + apt rootProject.ext.dependencies.dagger2Compiler + provided 'org.glassfish:javax.annotation:10.0-b28' + + testCompile rootProject.ext.dependencies.junit + testCompile rootProject.ext.dependencies.mockito + + androidTestCompile rootProject.ext.dependencies.mockito + androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.1' + androidTestCompile rootProject.ext.dependencies.supportAnnotations + androidTestCompile 'com.android.support.test:runner:0.4.1' + androidTestCompile 'com.android.support.test:rules:0.4.1' + androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1' + androidTestCompile 'org.hamcrest:hamcrest-library:1.3' } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro old mode 100644 new mode 100755 diff --git a/app/src/androidTest/java/android/coding/interview/makeitawesome/DetailActivityTest.java b/app/src/androidTest/java/android/coding/interview/makeitawesome/DetailActivityTest.java new file mode 100755 index 0000000..cc6df5d --- /dev/null +++ b/app/src/androidTest/java/android/coding/interview/makeitawesome/DetailActivityTest.java @@ -0,0 +1,19 @@ +package android.coding.interview.makeitawesome; + +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.runner.RunWith; + +/** + * Created by liang on 01/02/16. + */ +@RunWith(AndroidJUnit4.class) +public class DetailActivityTest { + //TODO + @Rule + public ActivityTestRule mActivityRule = new ActivityTestRule(DetailActivity.class); + + +} \ No newline at end of file diff --git a/app/src/androidTest/java/android/coding/interview/makeitawesome/MainActivityTest.java b/app/src/androidTest/java/android/coding/interview/makeitawesome/MainActivityTest.java new file mode 100755 index 0000000..b962213 --- /dev/null +++ b/app/src/androidTest/java/android/coding/interview/makeitawesome/MainActivityTest.java @@ -0,0 +1,236 @@ +package android.coding.interview.makeitawesome; + +import android.coding.interview.makeitawesome.di.AppComponent; +import android.coding.interview.makeitawesome.di.DaggerAppComponent; +import android.coding.interview.makeitawesome.domain.Repository; +import android.coding.interview.makeitawesome.domain.entity.Event; +import android.coding.interview.makeitawesome.domain.entity.Events; +import android.coding.interview.makeitawesome.domain.entity.Images; +import android.support.test.InstrumentationRegistry; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; + +import rx.Observable; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.action.ViewActions.swipeDown; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.matcher.RootMatchers.withDecorView; +import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withText; +import static org.hamcrest.Matchers.not; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +/** + * Created by liang on 01/02/16. + */ +@RunWith(AndroidJUnit4.class) +public class MainActivityTest { + public final ActivityTestRule mainActivityActivityTestRule = new ActivityTestRule<>(MainActivity.class); + + @Mock + Repository mMockRepository; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testErrorViewIsDisplayedWhenAnErrorIsReturned() throws InterruptedException { + + App app = (App) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext(); + + //create mock repository returns error; + when(mMockRepository.getNowShowingMovies(anyInt())).thenReturn(Observable.error(new Throwable("test"))); + AppComponent component = DaggerAppComponent.builder() + .appModule(new MockAppModule(app, mMockRepository)) + .build(); + app.setAppComponent(component); + + mainActivityActivityTestRule.launchActivity(null); + onView(withId(R.id.errorView)).check(matches(isDisplayed())); + onView(withId(R.id.contentView)).check(matches(not(isDisplayed()))); + onView(withId(R.id.emptyView)).check(matches(not(isDisplayed()))); + onView(withId(R.id.loadingView)).check(matches(not(isDisplayed()))); + } + + @Test + public void testContentViewIsDisplayedWhenAnEventsIsReturned() throws InterruptedException { + App app = (App) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext(); + + //create mock repository returns an Events instance + when(mMockRepository.getNowShowingMovies(anyInt())).thenReturn(Observable.just(createEvents())); + AppComponent component = DaggerAppComponent.builder() + .appModule(new MockAppModule(app, mMockRepository)) + .build(); + app.setAppComponent(component); + + mainActivityActivityTestRule.launchActivity(null); + onView(withId(R.id.contentView)).check(matches(isDisplayed())); + onView(withId(R.id.errorView)).check(matches(not(isDisplayed()))); + onView(withId(R.id.emptyView)).check(matches(not(isDisplayed()))); + onView(withId(R.id.loadingView)).check(matches(not(isDisplayed()))); + } + + @Test + public void testEmptyViewIsDisplayedWhenAnEventsWithEmptyEventListIsReturned() throws InterruptedException { + + App app = (App) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext(); + + //create mock repository returns an Events instance with empty Event list + Events events = new Events(); + events.setEvents(new ArrayList<>()); + when(mMockRepository.getNowShowingMovies(anyInt())).thenReturn(Observable.just(events)); + AppComponent component = DaggerAppComponent.builder() + .appModule(new MockAppModule(app, mMockRepository)) + .build(); + app.setAppComponent(component); + + mainActivityActivityTestRule.launchActivity(null); + onView(withId(R.id.errorView)).check(matches(not(isDisplayed()))); + onView(withId(R.id.loadingView)).check(matches(not(isDisplayed()))); + onView(withId(R.id.contentView)).check(matches(isDisplayed())); + onView(withId(R.id.emptyView)).check(matches(isDisplayed())); + } + + @Test + public void testLoadingViewIsDisplayedWhenAnRequestIsGoingOn() throws InterruptedException { + + App app = (App) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext(); + //create mock repository never returns result. + when(mMockRepository.getNowShowingMovies(anyInt())).thenReturn(Observable.never()); + AppComponent component = DaggerAppComponent.builder() + .appModule(new MockAppModule(app, mMockRepository)) + .build(); + app.setAppComponent(component); + + mainActivityActivityTestRule.launchActivity(null); + onView(withId(R.id.loadingView)).check(matches(isDisplayed())); + onView(withId(R.id.errorView)).check(matches(not(isDisplayed()))); + onView(withId(R.id.contentView)).check(matches(not(isDisplayed()))); + onView(withId(R.id.emptyView)).check(matches(not(isDisplayed()))); + } + + @Test + public void testToastMessageIsDisplayedWhenAnErrorReturnedTriggeredBySwipeRefreshLayout() throws InterruptedException { + + App app = (App) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext(); + //create mock repository returns an Events. + when(mMockRepository.getNowShowingMovies(anyInt())).thenReturn(Observable.just(mock(Events.class))); + AppComponent component = DaggerAppComponent.builder() + .appModule(new MockAppModule(app, mMockRepository)) + .build(); + app.setAppComponent(component); + + mainActivityActivityTestRule.launchActivity(null); + + //create mock repository returns an error. + when(mMockRepository.getNowShowingMovies(anyInt())).thenReturn(Observable.error(new Throwable("test error"))); + onView(withId(R.id.contentView)).perform(swipeDown()); + onView(withId(R.id.loadingView)).check(matches(not(isDisplayed()))); + onView(withId(R.id.errorView)).check(matches(not(isDisplayed()))); + onView(withId(R.id.contentView)).check(matches(isDisplayed())); + + //verify if Toast message is displayed + onView(withText("test error")) + .inRoot(withDecorView(not(mainActivityActivityTestRule.getActivity().getWindow().getDecorView()))) + .check(matches(isDisplayed())); + } + + @Test + public void testLoadingViewIsDisplayedWhenErrorViewIsClicked() throws InterruptedException { + + App app = (App) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext(); + + //create mock repository returns error; + when(mMockRepository.getNowShowingMovies(anyInt())).thenReturn(Observable.error(new Throwable("test"))); + AppComponent component = DaggerAppComponent.builder() + .appModule(new MockAppModule(app, mMockRepository)) + .build(); + app.setAppComponent(component); + + mainActivityActivityTestRule.launchActivity(null); + + //create mock repository never returns result; + when(mMockRepository.getNowShowingMovies(anyInt())).thenReturn(Observable.never()); + onView(withId(R.id.errorView)).check(matches(isDisplayed())); + onView(withId(R.id.errorView)).perform(click()); + onView(withId(R.id.errorView)).check(matches(not(isDisplayed()))); + onView(withId(R.id.contentView)).check(matches(not(isDisplayed()))); + onView(withId(R.id.emptyView)).check(matches(not(isDisplayed()))); + onView(withId(R.id.loadingView)).check(matches(isDisplayed())); + } + + @Test + public void testContentViewIsDisplayedWhenErrorViewIsClicked() throws InterruptedException { + + App app = (App) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext(); + + //create mock repository returns error; + when(mMockRepository.getNowShowingMovies(anyInt())).thenReturn(Observable.error(new Throwable("test"))); + AppComponent component = DaggerAppComponent.builder() + .appModule(new MockAppModule(app, mMockRepository)) + .build(); + app.setAppComponent(component); + + mainActivityActivityTestRule.launchActivity(null); + + //create mock repository returns an Event; + when(mMockRepository.getNowShowingMovies(anyInt())).thenReturn(Observable.just(mock(Events.class))); + onView(withId(R.id.errorView)).check(matches(isDisplayed())); + onView(withId(R.id.errorView)).perform(click()); + onView(withId(R.id.errorView)).check(matches(not(isDisplayed()))); + onView(withId(R.id.contentView)).check(matches(isDisplayed())); + onView(withId(R.id.loadingView)).check(matches(not(isDisplayed()))); + } + + @Test + public void testErrorViewIsDisplayedWhenErrorViewIsClicked() throws InterruptedException { + + App app = (App) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext(); + + //create mock repository returns error; + when(mMockRepository.getNowShowingMovies(anyInt())).thenReturn(Observable.error(new Throwable("test"))); + AppComponent component = DaggerAppComponent.builder() + .appModule(new MockAppModule(app, mMockRepository)) + .build(); + app.setAppComponent(component); + + mainActivityActivityTestRule.launchActivity(null); + + onView(withId(R.id.errorView)).check(matches(isDisplayed())); + onView(withId(R.id.errorView)).perform(click()); + onView(withId(R.id.errorView)).check(matches(isDisplayed())); + onView(withId(R.id.contentView)).check(matches(not(isDisplayed()))); + onView(withId(R.id.loadingView)).check(matches(not(isDisplayed()))); + + } + + private Events createEvents() { + Events events = new Events(); + Event event = new Event(); + event.setTitle("test"); + Images images = new Images(); + images.setEventMediumImagePortrait("http://media.finnkino.fi/1012/Event_10655/portrait_medium/Zoolander2_1080t.jpg"); + event.setImages(images); + List eventList = new ArrayList<>(); + eventList.add(event); + events.setEvents(eventList); + return events; + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/android/coding/interview/makeitawesome/MockAppModule.java b/app/src/androidTest/java/android/coding/interview/makeitawesome/MockAppModule.java new file mode 100755 index 0000000..cc3b231 --- /dev/null +++ b/app/src/androidTest/java/android/coding/interview/makeitawesome/MockAppModule.java @@ -0,0 +1,26 @@ +package android.coding.interview.makeitawesome; + +import android.app.Application; +import android.coding.interview.makeitawesome.data.RepositoryImpl; +import android.coding.interview.makeitawesome.di.AppModule; +import android.coding.interview.makeitawesome.domain.Repository; + +/** + * Mock module to facilitate testing + * + * Created by liang on 01/02/16. + */ +public class MockAppModule extends AppModule { + + private Repository mRepository; + + public MockAppModule(Application app, Repository repository) { + super(app); + mRepository = repository; + } + + @Override + public Repository provideRepository(RepositoryImpl repository) { + return mRepository; + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml old mode 100644 new mode 100755 index b52b269..10efd8a --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,21 +1,27 @@ + package="android.coding.interview.makeitawesome"> + + + + + android:theme="@style/AppTheme"> + android:label="@string/app_name"> + diff --git a/app/src/main/java/android/coding/interview/makeitawesome/App.java b/app/src/main/java/android/coding/interview/makeitawesome/App.java new file mode 100755 index 0000000..d5e5884 --- /dev/null +++ b/app/src/main/java/android/coding/interview/makeitawesome/App.java @@ -0,0 +1,30 @@ +package android.coding.interview.makeitawesome; + +import android.app.Application; +import android.coding.interview.makeitawesome.di.AppComponent; +import android.coding.interview.makeitawesome.di.AppModule; +import android.coding.interview.makeitawesome.di.DaggerAppComponent; + +import com.squareup.leakcanary.LeakCanary; + +/** + * Created by Liang on 2016/1/30. + */ +public class App extends Application { + private AppComponent mAppComponent; + + @Override + public void onCreate() { + super.onCreate(); + LeakCanary.install(this); + mAppComponent = DaggerAppComponent.builder().appModule(new AppModule(this)).build(); + } + + public AppComponent getAppComponent() { + return mAppComponent; + } + + public void setAppComponent(AppComponent appComponent) { + this.mAppComponent = appComponent; + } +} diff --git a/app/src/main/java/android/coding/interview/makeitawesome/DetailActivity.java b/app/src/main/java/android/coding/interview/makeitawesome/DetailActivity.java new file mode 100755 index 0000000..56ab1dc --- /dev/null +++ b/app/src/main/java/android/coding/interview/makeitawesome/DetailActivity.java @@ -0,0 +1,101 @@ +package android.coding.interview.makeitawesome; + +import android.app.Activity; +import android.coding.interview.makeitawesome.di.DaggerDetailActivityComponent; +import android.coding.interview.makeitawesome.di.DetailActivityComponent; +import android.coding.interview.makeitawesome.domain.entity.Events; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.ActivityOptionsCompat; +import android.support.v4.util.Pair; +import android.support.v4.view.ViewCompat; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import com.squareup.picasso.Picasso; + +import javax.inject.Inject; + +import au.com.gridstone.rxstore.RxStore; +import rx.android.schedulers.AndroidSchedulers; +import rx.schedulers.Schedulers; + +/** + *Simple Detail View of a Finnkio movie event, not all the information is showed. + */ +public class DetailActivity extends AppCompatActivity { + + private static final String TAG = DetailActivity.class.getSimpleName(); + private static final String TRANSITION_NAME_COVER = "cover"; + private static final String KEY = "key"; + private static final String POSITION = "position"; + private ImageView mCoverImageView; + private TextView mTitle; + private TextView mSynopsis; + private TextView mEventLink; + + @Inject + RxStore mRxStore; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_detail); + injectDependencies(); + initViews(); + + String key = getIntent().getExtras().getString(KEY); + int position = getIntent().getExtras().getInt(POSITION, 0); + Log.d(TAG, "key: " + key); + Log.d(TAG, "position: " + position); + if (key == null) + onBackPressed(); + ViewCompat.setTransitionName(mCoverImageView, TRANSITION_NAME_COVER); + mRxStore.get(key, Events.class) + .map(events -> events.getEvents().get(position)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(event -> { + Log.d(TAG, "event: " + event.getTitle()); + Picasso.with(DetailActivity.this) + .load(event.getImages().getEventMediumImagePortrait()) + .fit() + .centerCrop() + .into(mCoverImageView); + mTitle.setText(event.getTitle()); + mSynopsis.setText(event.getSynopsis()); + mEventLink.setText(event.getEventURL()); + mSynopsis.setVisibility(View.VISIBLE); + mEventLink.setVisibility(View.VISIBLE); + }, Throwable::printStackTrace); + } + + public static void launch(Activity activity, View cover, String key, int position) { + ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, + Pair.create(cover, TRANSITION_NAME_COVER)); + Intent intent = new Intent(activity, DetailActivity.class); + intent.putExtra(KEY, key); + intent.putExtra(POSITION, position); + ActivityCompat.startActivity(activity, intent, options.toBundle()); + } + + private void initViews() { + mTitle = (TextView) findViewById(R.id.activity_detail_title); + mSynopsis = (TextView) findViewById(R.id.activity_detail_synopsis); + mEventLink = (TextView) findViewById(R.id.activity_detail_event_link); + mCoverImageView = (ImageView) findViewById(R.id.item_movie_cover); + } + + private void injectDependencies() { + App app = (App) getApplication(); + DetailActivityComponent component = DaggerDetailActivityComponent + .builder() + .appComponent(app.getAppComponent()) + .build(); + component.inject(this); + } +} diff --git a/app/src/main/java/android/coding/interview/makeitawesome/MainActivity.java b/app/src/main/java/android/coding/interview/makeitawesome/MainActivity.java old mode 100644 new mode 100755 index 3a779fa..cb4856c --- a/app/src/main/java/android/coding/interview/makeitawesome/MainActivity.java +++ b/app/src/main/java/android/coding/interview/makeitawesome/MainActivity.java @@ -1,7 +1,6 @@ package android.coding.interview.makeitawesome; -import android.coding.interview.makeitawesome.fragment.PicturesFragment; -import android.coding.interview.makeitawesome.fragment.WelcomeScreenFragment; +import android.coding.interview.makeitawesome.fragment.NowShowingInTheatresFragment; import android.os.Bundle; import android.support.design.widget.NavigationView; import android.support.v4.app.FragmentManager; @@ -26,25 +25,25 @@ protected void onCreate(Bundle savedInstanceState) { // setup action bar Toolbar actionBar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(actionBar); - + // setting up action bar icon and home navigation final ActionBar ab = getSupportActionBar(); ab.setHomeAsUpIndicator(R.drawable.ic_menu); ab.setDisplayHomeAsUpEnabled(true); - - + + // drawer layout mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); NavigationView navigationView = (NavigationView) findViewById(R.id.navigation_view); if (navigationView != null) { - navigationView.setNavigationItemSelectedListener(this); + navigationView.setNavigationItemSelectedListener(this); } FragmentManager fm = getSupportFragmentManager(); if (fm.findFragmentById(R.id.content_frame) == null) { // no fragment visible yet -> create default one - fm.beginTransaction().add(R.id.content_frame, WelcomeScreenFragment.newInstance()).commit(); + fm.beginTransaction().add(R.id.content_frame, NowShowingInTheatresFragment.newInstance(), "PICTURES_FRAGMENT").commit(); } - + } @Override @@ -56,7 +55,7 @@ public boolean onCreateOptionsMenu(Menu menu) { @Override public boolean onOptionsItemSelected(MenuItem item) { - switch(item.getItemId()) { + switch (item.getItemId()) { case android.R.id.home: mDrawerLayout.openDrawer(GravityCompat.START); return true; @@ -69,13 +68,13 @@ public boolean onNavigationItemSelected(MenuItem menuItem) { // handling items selection in Menu Drawer //menuItem.setChecked(true); switch (menuItem.getItemId()) { - case R.id.navigation_photos: + case R.id.now_showing: FragmentManager fm = getSupportFragmentManager(); if (fm.findFragmentByTag("PICTURES_FRAGMENT") == null) { // no pictures shown yet, show it - fm.beginTransaction().replace(R.id.content_frame, PicturesFragment.newInstance(), "PICTURES_FRAGMENT").addToBackStack(null).commit(); + fm.beginTransaction().replace(R.id.content_frame, NowShowingInTheatresFragment.newInstance(), "PICTURES_FRAGMENT").addToBackStack(null).commit(); } } - + mDrawerLayout.closeDrawers(); return true; } diff --git a/app/src/main/java/android/coding/interview/makeitawesome/adapter/EventsAdapter.java b/app/src/main/java/android/coding/interview/makeitawesome/adapter/EventsAdapter.java new file mode 100755 index 0000000..1697047 --- /dev/null +++ b/app/src/main/java/android/coding/interview/makeitawesome/adapter/EventsAdapter.java @@ -0,0 +1,73 @@ +package android.coding.interview.makeitawesome.adapter; + +import android.coding.interview.makeitawesome.R; +import android.coding.interview.makeitawesome.domain.entity.Event; +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.squareup.picasso.Picasso; + +import java.util.List; + +/** + * adapter keeping data for list of movie events + */ +public class EventsAdapter extends RecyclerView.Adapter { + + //private static final String TAG = PhotosAdapter.class.getSimpleName(); + private List mData; + private Context mContext; + + public EventsAdapter(List mData) { + this.mData = mData; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int i) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.picture_list_item, parent, false); + this.mContext = parent.getContext(); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int i) { + holder.mMovieTitle.setText(mData.get(i).getTitle()); + Picasso.with(mContext) + .load(mData.get(i).getImages().getEventMediumImagePortrait()) + .fit() + .centerCrop() + .into(holder.mMovieCover); + } + + @Override + public int getItemCount() { + return mData.size(); + } + + public List getData() { + return mData; + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + final TextView mMovieTitle; + final ImageView mMovieCover; + final View container; + + public ViewHolder(View itemView) { + super(itemView); + mMovieTitle = (TextView) itemView.findViewById(R.id.item_movie_title); + mMovieCover = (ImageView) itemView.findViewById(R.id.item_movie_cover); + container = itemView; + } + } + + public void refreshData(List data) { + mData = data; + notifyDataSetChanged(); + } +} diff --git a/app/src/main/java/android/coding/interview/makeitawesome/adapter/PhotosAdapter.java b/app/src/main/java/android/coding/interview/makeitawesome/adapter/PhotosAdapter.java deleted file mode 100644 index 366168c..0000000 --- a/app/src/main/java/android/coding/interview/makeitawesome/adapter/PhotosAdapter.java +++ /dev/null @@ -1,45 +0,0 @@ -package android.coding.interview.makeitawesome.adapter; - -import android.coding.interview.makeitawesome.R; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import java.util.Arrays; -import java.util.List; - -/** - * adapter keeping data for list of photos - */ -public class PhotosAdapter extends RecyclerView.Adapter { - - // TODO this is a dummy data that you have to replace. You probably need List of some objects representing pictures - // rather than just strings - private List mData = Arrays.asList("dummy 1", "dummy 2", "dummy 3", "dummy 4", "etc"); - - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent, int i) { - View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.picture_list_item, parent, false); - return new ViewHolder(view); - } - - @Override - public void onBindViewHolder(ViewHolder holder, int i) { - holder.mText.setText(mData.get(i)); - } - - @Override - public int getItemCount() { - return mData.size(); - } - - public static class ViewHolder extends RecyclerView.ViewHolder { - final TextView mText; - public ViewHolder(View itemView) { - super(itemView); - mText = (TextView) itemView; - } - } -} diff --git a/app/src/main/java/android/coding/interview/makeitawesome/base/presenter/BaseRxLcePresenter.java b/app/src/main/java/android/coding/interview/makeitawesome/base/presenter/BaseRxLcePresenter.java new file mode 100755 index 0000000..cbfc3b9 --- /dev/null +++ b/app/src/main/java/android/coding/interview/makeitawesome/base/presenter/BaseRxLcePresenter.java @@ -0,0 +1,96 @@ +package android.coding.interview.makeitawesome.base.presenter; + +import com.hannesdorfmann.mosby.mvp.lce.MvpLceView; + +import rx.Observable; +import rx.Subscriber; +import rx.android.schedulers.AndroidSchedulers; +import rx.schedulers.Schedulers; + +/** + * A presenter for RxJava, that assumes that only one Observable is subscribed by this presenter. + * The idea is, that you make your (chain of) Observable and pass it to {@link + * #subscribe(Observable, boolean)}. The presenter internally subscribes himself as Subscriber to + * the observable(which executes the observable). + * + */ +public class BaseRxLcePresenter, M> + extends com.hannesdorfmann.mosby.mvp.MvpBasePresenter { + + protected Subscriber mSubscriber; + + /** + * Unsubscribes the subscriber and set it to null + */ + protected void unsubscribe() { + if (mSubscriber != null && !mSubscriber.isUnsubscribed()) { + mSubscriber.unsubscribe(); + } + + mSubscriber = null; + } + + /** + * Subscribes the presenter himself as subscriber on the observable + * + * @param observable The observable to subscribe + * @param pullToRefresh Pull to refresh? + */ + public void subscribe(Observable observable, final boolean pullToRefresh) { + + if (isViewAttached()) { + getView().showLoading(pullToRefresh); + } + + unsubscribe(); + + mSubscriber = new Subscriber() { + private boolean ptr = pullToRefresh; + + @Override public void onCompleted() { + BaseRxLcePresenter.this.onCompleted(); + } + + @Override public void onError(Throwable e) { + BaseRxLcePresenter.this.onError(e, ptr); + } + + @Override public void onNext(M m) { + BaseRxLcePresenter.this.onNext(m); + } + }; + + observable + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(mSubscriber); + } + + protected void onCompleted() { + if (isViewAttached()) { + getView().showContent(); + } + unsubscribe(); + } + + protected void onError(Throwable e, boolean pullToRefresh) { + if (isViewAttached()) { + getView().showError(e, pullToRefresh); + } + unsubscribe(); + } + + protected void onNext(M data) { + if (isViewAttached()) { + getView().setData(data); + } + } + + @Override public void detachView(boolean retainInstance) { + super.detachView(retainInstance); + if (!retainInstance) { + unsubscribe(); + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/android/coding/interview/makeitawesome/base/view/BaseLceFragment.java b/app/src/main/java/android/coding/interview/makeitawesome/base/view/BaseLceFragment.java new file mode 100755 index 0000000..0d6a85b --- /dev/null +++ b/app/src/main/java/android/coding/interview/makeitawesome/base/view/BaseLceFragment.java @@ -0,0 +1,56 @@ +package android.coding.interview.makeitawesome.base.view; + +import android.coding.interview.makeitawesome.App; +import android.coding.interview.makeitawesome.di.AppComponent; +import android.os.Bundle; +import android.support.annotation.LayoutRes; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.hannesdorfmann.mosby.mvp.MvpPresenter; +import com.hannesdorfmann.mosby.mvp.lce.MvpLceView; +import com.hannesdorfmann.mosby.mvp.viewstate.lce.MvpLceViewStateFragment; + +public abstract class BaseLceFragment, P extends MvpPresenter> + extends MvpLceViewStateFragment { + + @Override public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @LayoutRes + protected abstract int getLayoutRes(); + + @Nullable @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(getLayoutRes(), container, false); + } + + @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + injectDependencies(); + super.onViewCreated(view, savedInstanceState); + //ButterKnife.bind(this, view); + } + + @Override public void onDestroyView() { + super.onDestroyView(); + //ButterKnife.unbind(this); + } + + + /** + * Inject the dependencies + */ + protected void injectDependencies() { + + } + + protected AppComponent getAppComponent() { + App app = (App) getActivity().getApplication(); + return app.getAppComponent(); + } + +} \ No newline at end of file diff --git a/app/src/main/java/android/coding/interview/makeitawesome/base/view/EmptyRecyclerView.java b/app/src/main/java/android/coding/interview/makeitawesome/base/view/EmptyRecyclerView.java new file mode 100755 index 0000000..97f129b --- /dev/null +++ b/app/src/main/java/android/coding/interview/makeitawesome/base/view/EmptyRecyclerView.java @@ -0,0 +1,59 @@ +package android.coding.interview.makeitawesome.base.view; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.View; + +public class EmptyRecyclerView extends RecyclerView { + @Nullable View mEmptyView; + + public EmptyRecyclerView(Context context) { + super(context); + } + + public EmptyRecyclerView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public EmptyRecyclerView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + void checkIfEmpty() { + if (mEmptyView != null) { + mEmptyView.setVisibility(getAdapter().getItemCount() > 0 ? GONE : VISIBLE); + } + } + + final @NonNull + AdapterDataObserver observer = new AdapterDataObserver() { + @Override public void onChanged() { + super.onChanged(); + checkIfEmpty(); + } + }; + + @Override public void setAdapter(@Nullable Adapter adapter) { + final RecyclerView.Adapter oldAdapter = getAdapter(); + if (oldAdapter != null) { + oldAdapter.unregisterAdapterDataObserver(observer); + } + super.setAdapter(adapter); + if (adapter != null) { + adapter.registerAdapterDataObserver(observer); + } + } + + public void setEmptyView(@Nullable View emptyView) { + this.mEmptyView = emptyView; + checkIfEmpty(); + } + + @Nullable + public View getEmptyView() { + return mEmptyView; + } +} \ No newline at end of file diff --git a/app/src/main/java/android/coding/interview/makeitawesome/di/AppComponent.java b/app/src/main/java/android/coding/interview/makeitawesome/di/AppComponent.java new file mode 100755 index 0000000..42485c8 --- /dev/null +++ b/app/src/main/java/android/coding/interview/makeitawesome/di/AppComponent.java @@ -0,0 +1,18 @@ +package android.coding.interview.makeitawesome.di; + +import android.coding.interview.makeitawesome.domain.Repository; + +import javax.inject.Singleton; + +import au.com.gridstone.rxstore.RxStore; +import dagger.Component; + +/** + * Created by Liang on 2016/1/31. + */ +@Singleton +@Component(modules = AppModule.class) +public interface AppComponent { + Repository repository(); + RxStore rxStore(); +} diff --git a/app/src/main/java/android/coding/interview/makeitawesome/di/AppModule.java b/app/src/main/java/android/coding/interview/makeitawesome/di/AppModule.java new file mode 100755 index 0000000..fee48f9 --- /dev/null +++ b/app/src/main/java/android/coding/interview/makeitawesome/di/AppModule.java @@ -0,0 +1,43 @@ +package android.coding.interview.makeitawesome.di; + +import android.app.Application; +import android.coding.interview.makeitawesome.data.FinnkinoApi; +import android.coding.interview.makeitawesome.data.RepositoryImpl; +import android.coding.interview.makeitawesome.domain.Repository; + +import javax.inject.Singleton; + +import au.com.gridstone.rxstore.RxStore; +import au.com.gridstone.rxstore.converters.GsonConverter; +import dagger.Module; +import dagger.Provides; + +/** + * Created by Liang on 2016/1/31. + */ +@Module +public class AppModule { + private final Application mApplication; + + public AppModule(Application application) { + this.mApplication = application; + } + + @Singleton + @Provides + public FinnkinoApi provideFinnkinoApi() { + return FinnkinoApi.Factory.create(); + } + + @Singleton + @Provides + public RxStore provideRxStore() { + return RxStore.withContext(mApplication).using(new GsonConverter()); + } + + @Singleton + @Provides + public Repository provideRepository(RepositoryImpl repository) { + return repository; + } +} diff --git a/app/src/main/java/android/coding/interview/makeitawesome/di/DetailActivityComponent.java b/app/src/main/java/android/coding/interview/makeitawesome/di/DetailActivityComponent.java new file mode 100755 index 0000000..4f875aa --- /dev/null +++ b/app/src/main/java/android/coding/interview/makeitawesome/di/DetailActivityComponent.java @@ -0,0 +1,14 @@ +package android.coding.interview.makeitawesome.di; + +import android.coding.interview.makeitawesome.DetailActivity; + +import dagger.Component; + +/** + * Created by Liang on 2016/2/1. + */ +@Component(dependencies = AppComponent.class) +@PerActivity +public interface DetailActivityComponent { + void inject(DetailActivity activity); +} diff --git a/app/src/main/java/android/coding/interview/makeitawesome/di/MoviesModule.java b/app/src/main/java/android/coding/interview/makeitawesome/di/MoviesModule.java new file mode 100755 index 0000000..116665b --- /dev/null +++ b/app/src/main/java/android/coding/interview/makeitawesome/di/MoviesModule.java @@ -0,0 +1,27 @@ +package android.coding.interview.makeitawesome.di; + +import android.coding.interview.makeitawesome.domain.interactor.GetNowShowingMoviesUseCase; +import android.coding.interview.makeitawesome.domain.interactor.GetNowShowingMoviesUseCaseImpl; +import android.coding.interview.makeitawesome.domain.interactor.GetUpcomingMoviesUseCase; +import android.coding.interview.makeitawesome.domain.interactor.GetUpcomingMoviesUseCaseImpl; + +import dagger.Module; +import dagger.Provides; + +/** + * Created by Liang on 2016/1/31. + */ +@Module +public class MoviesModule { + @PerFragment + @Provides + GetUpcomingMoviesUseCase provideUpcomingMoviesUseCase(GetUpcomingMoviesUseCaseImpl getUpcomingMoviesUseCase) { + return getUpcomingMoviesUseCase; + } + + @PerFragment + @Provides + GetNowShowingMoviesUseCase provideNowShowingMoviesUseCase(GetNowShowingMoviesUseCaseImpl getNowShowingMoviesUseCase) { + return getNowShowingMoviesUseCase; + } +} diff --git a/app/src/main/java/android/coding/interview/makeitawesome/di/PerActivity.java b/app/src/main/java/android/coding/interview/makeitawesome/di/PerActivity.java new file mode 100755 index 0000000..d13431e --- /dev/null +++ b/app/src/main/java/android/coding/interview/makeitawesome/di/PerActivity.java @@ -0,0 +1,15 @@ +package android.coding.interview.makeitawesome.di; + +import java.lang.annotation.Retention; + +import javax.inject.Scope; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Created by Liang on 2016/2/1. + */ +@Scope +@Retention(RUNTIME) +public @interface PerActivity { +} diff --git a/app/src/main/java/android/coding/interview/makeitawesome/di/PerFragment.java b/app/src/main/java/android/coding/interview/makeitawesome/di/PerFragment.java new file mode 100755 index 0000000..49cd79a --- /dev/null +++ b/app/src/main/java/android/coding/interview/makeitawesome/di/PerFragment.java @@ -0,0 +1,15 @@ +package android.coding.interview.makeitawesome.di; + +import java.lang.annotation.Retention; + +import javax.inject.Scope; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Created by Liang on 2016/1/31. + */ +@Scope +@Retention(RUNTIME) +public @interface PerFragment { +} diff --git a/app/src/main/java/android/coding/interview/makeitawesome/di/PicturesFragmentComponent.java b/app/src/main/java/android/coding/interview/makeitawesome/di/PicturesFragmentComponent.java new file mode 100755 index 0000000..dace647 --- /dev/null +++ b/app/src/main/java/android/coding/interview/makeitawesome/di/PicturesFragmentComponent.java @@ -0,0 +1,11 @@ +package android.coding.interview.makeitawesome.di; + +import android.coding.interview.makeitawesome.presenter.NowShowingInTheatresPresenter; + +import dagger.Component; + +@Component(dependencies = AppComponent.class, modules = MoviesModule.class) +@PerFragment +public interface PicturesFragmentComponent { + NowShowingInTheatresPresenter presenter(); +} diff --git a/app/src/main/java/android/coding/interview/makeitawesome/fragment/NowShowingInTheatresFragment.java b/app/src/main/java/android/coding/interview/makeitawesome/fragment/NowShowingInTheatresFragment.java new file mode 100755 index 0000000..358e8d2 --- /dev/null +++ b/app/src/main/java/android/coding/interview/makeitawesome/fragment/NowShowingInTheatresFragment.java @@ -0,0 +1,182 @@ +package android.coding.interview.makeitawesome.fragment; + +import android.coding.interview.makeitawesome.DetailActivity; +import android.coding.interview.makeitawesome.R; +import android.coding.interview.makeitawesome.adapter.EventsAdapter; +import android.coding.interview.makeitawesome.base.view.BaseLceFragment; +import android.coding.interview.makeitawesome.base.view.EmptyRecyclerView; +import android.coding.interview.makeitawesome.di.DaggerPicturesFragmentComponent; +import android.coding.interview.makeitawesome.di.MoviesModule; +import android.coding.interview.makeitawesome.di.PicturesFragmentComponent; +import android.coding.interview.makeitawesome.domain.Repository; +import android.coding.interview.makeitawesome.domain.entity.Event; +import android.coding.interview.makeitawesome.presenter.NowShowingInTheatresPresenter; +import android.coding.interview.makeitawesome.utils.ItemClickSupport; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.widget.TextView; + +import com.hannesdorfmann.mosby.mvp.viewstate.lce.LceViewState; +import com.hannesdorfmann.mosby.mvp.viewstate.lce.data.RetainingFragmentLceViewState; + +import java.util.ArrayList; +import java.util.List; + +/** + * This fragment fetches the NowInTheatres events from Finnkino server and shows them + * + * @see Finnkino Api + */ +public class NowShowingInTheatresFragment extends BaseLceFragment, NowShowingInTheatresView, NowShowingInTheatresPresenter> + implements SwipeRefreshLayout.OnRefreshListener, + ItemClickSupport.OnItemClickListener { + + private static final String TAG = NowShowingInTheatresFragment.class.getSimpleName(); + + public static Fragment newInstance() { + return new NowShowingInTheatresFragment(); + } + + private EventsAdapter mAdapter; + + + public EmptyRecyclerView emptyRecyclerView; + + + public TextView emptyView; + + PicturesFragmentComponent component; + + @Override + protected int getLayoutRes() { + return R.layout.pictures_list_fragment; + } + + @NonNull + @Override + public NowShowingInTheatresPresenter createPresenter() { + return component.presenter(); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + emptyRecyclerView = (EmptyRecyclerView) view.findViewById(R.id.recyclerView); + emptyView = (TextView) view.findViewById(R.id.emptyView); + contentView.setOnRefreshListener(this); + setupRecyclerView(emptyRecyclerView); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + } + + @Override + protected String getErrorMessage(Throwable e, boolean pullToRefresh) { + return e.getMessage(); + } + + /** + * Show content view + */ + @Override + public void showContent() { + super.showContent(); + if (emptyRecyclerView.getEmptyView() == null) emptyRecyclerView.setEmptyView(emptyView); + //contentView.setRefreshing(false); + contentView.post(() -> contentView.setRefreshing(false)); + } + + /** + * Show error view + * + * @param e error to be showed + * @param pullToRefresh isPullToRefresh + */ + @Override + public void showError(Throwable e, boolean pullToRefresh) { + super.showError(e, pullToRefresh); + contentView.setRefreshing(false); + } + + /** + * Show loading view + * + * @param pullToRefresh isPullToRefresh + */ + @Override + public void showLoading(boolean pullToRefresh) { + super.showLoading(pullToRefresh); + if (pullToRefresh && !contentView.isRefreshing()) { + // Workaround for measure bug: https://code.google.com/p/android/issues/detail?id=77712 + contentView.post(() -> contentView.setRefreshing(true)); + } + } + + @NonNull + @Override + public LceViewState, NowShowingInTheatresView> createViewState() { + return new RetainingFragmentLceViewState<>(this); + } + + @Override + public List getData() { + return mAdapter.getData(); + } + + @Override + public void setData(List data) { + mAdapter.refreshData(data); + } + + @Override + public void loadData(boolean pullToRefresh) { + presenter.getUpComingMovies(pullToRefresh); + } + + @Override + protected void injectDependencies() { + component = DaggerPicturesFragmentComponent + .builder() + .appComponent(getAppComponent()) + .moviesModule(new MoviesModule()) + .build(); + } + + /** + * SwipeRefreshLayout refresh callback + */ + @Override + public void onRefresh() { + loadData(true); + } + + private void setupRecyclerView(RecyclerView recyclerView) { + recyclerView.setLayoutManager(new GridLayoutManager(getActivity(), + getResources().getInteger(R.integer.span_count_movie))); + mAdapter = new EventsAdapter(new ArrayList<>()); + recyclerView.setAdapter(mAdapter); + ItemClickSupport.addTo(recyclerView).setOnItemClickListener(this); + } + + /** + * Callback method to be invoked when an item in the RecyclerView + * has been clicked. + * + * @param recyclerView The RecyclerView where the click happened. + * @param view The view within the RecyclerView that was clicked + * (this will be a view provided by the adapter). + * @param position The position of the view in the adapter. + */ + @Override + public void onItemClicked(RecyclerView recyclerView, View view, int position) { + DetailActivity.launch(getActivity(), view.findViewById(R.id.item_movie_cover), Repository.KEY_EVENTS_NOW_IN_THEATRES, position); + } +} diff --git a/app/src/main/java/android/coding/interview/makeitawesome/fragment/NowShowingInTheatresView.java b/app/src/main/java/android/coding/interview/makeitawesome/fragment/NowShowingInTheatresView.java new file mode 100755 index 0000000..660ead5 --- /dev/null +++ b/app/src/main/java/android/coding/interview/makeitawesome/fragment/NowShowingInTheatresView.java @@ -0,0 +1,13 @@ +package android.coding.interview.makeitawesome.fragment; + +import android.coding.interview.makeitawesome.domain.entity.Event; + +import com.hannesdorfmann.mosby.mvp.lce.MvpLceView; + +import java.util.List; + +/** + * Created by Liang on 2016/1/31. + */ +public interface NowShowingInTheatresView extends MvpLceView>{ +} diff --git a/app/src/main/java/android/coding/interview/makeitawesome/fragment/PicturesFragment.java b/app/src/main/java/android/coding/interview/makeitawesome/fragment/PicturesFragment.java deleted file mode 100644 index 32a7c34..0000000 --- a/app/src/main/java/android/coding/interview/makeitawesome/fragment/PicturesFragment.java +++ /dev/null @@ -1,37 +0,0 @@ -package android.coding.interview.makeitawesome.fragment; - -import android.coding.interview.makeitawesome.R; -import android.coding.interview.makeitawesome.adapter.PhotosAdapter; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -/** - * This is Your TASK:
- * This is a fragment where you need to show list of pictures and details fetched from API
- * Most of skeleton for showing UI is implemented. You need to take care of getting the data from server, updating adapter and displaying results - */ -public class PicturesFragment extends Fragment { - - public static Fragment newInstance() { - return new PicturesFragment(); - } - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - RecyclerView rv = (RecyclerView) inflater.inflate(R.layout.pictures_list_fragment, container, false); - setupRecyclerView(rv); - return rv; - } - - private void setupRecyclerView(RecyclerView recyclerView) { - recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext())); - recyclerView.setAdapter(new PhotosAdapter()); - } -} diff --git a/app/src/main/java/android/coding/interview/makeitawesome/fragment/WelcomeScreenFragment.java b/app/src/main/java/android/coding/interview/makeitawesome/fragment/WelcomeScreenFragment.java old mode 100644 new mode 100755 diff --git a/app/src/main/java/android/coding/interview/makeitawesome/presenter/NowShowingInTheatresPresenter.java b/app/src/main/java/android/coding/interview/makeitawesome/presenter/NowShowingInTheatresPresenter.java new file mode 100755 index 0000000..4c2cbd9 --- /dev/null +++ b/app/src/main/java/android/coding/interview/makeitawesome/presenter/NowShowingInTheatresPresenter.java @@ -0,0 +1,28 @@ +package android.coding.interview.makeitawesome.presenter; + +import android.coding.interview.makeitawesome.base.presenter.BaseRxLcePresenter; +import android.coding.interview.makeitawesome.data.FinnkinoApi; +import android.coding.interview.makeitawesome.domain.entity.Event; +import android.coding.interview.makeitawesome.domain.entity.Events; +import android.coding.interview.makeitawesome.domain.interactor.GetNowShowingMoviesUseCase; +import android.coding.interview.makeitawesome.fragment.NowShowingInTheatresView; + +import java.util.List; + +import javax.inject.Inject; + +/** + * Created by Liang on 2016/1/31. + */ +public class NowShowingInTheatresPresenter extends BaseRxLcePresenter> { + private final GetNowShowingMoviesUseCase mGetNowShowingMoviesUseCase; + + @Inject + public NowShowingInTheatresPresenter(GetNowShowingMoviesUseCase getUpcomingMoviesUseCase) { + mGetNowShowingMoviesUseCase = getUpcomingMoviesUseCase; + } + + public void getUpComingMovies(boolean pullToRefresh) { + subscribe(mGetNowShowingMoviesUseCase.execute(FinnkinoApi.AreaID.Helsinki).map(Events::getEvents), pullToRefresh); + } +} diff --git a/app/src/main/java/android/coding/interview/makeitawesome/utils/ItemClickSupport.java b/app/src/main/java/android/coding/interview/makeitawesome/utils/ItemClickSupport.java new file mode 100755 index 0000000..73fd025 --- /dev/null +++ b/app/src/main/java/android/coding/interview/makeitawesome/utils/ItemClickSupport.java @@ -0,0 +1,147 @@ +package android.coding.interview.makeitawesome.utils; + +import android.coding.interview.makeitawesome.R; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +import static android.support.v7.widget.RecyclerView.ViewHolder; +import static android.view.View.OnClickListener; +import static android.view.View.OnLongClickListener; + +/** + * Modified version of the implementation by Hugo Visser. + * See Getting your + * clicks on RecyclerView. + */ +public final class ItemClickSupport { + private final RecyclerView mRecyclerView; + + private OnItemClickListener mOnItemClickListener; + private OnItemLongClickListener mOnItemLongClickListener; + + private final OnClickListener mOnClickListener = new OnClickListener() { + @Override + public void onClick(View v) { + if (mOnItemClickListener != null) { + ViewHolder holder = mRecyclerView.getChildViewHolder(v); + mOnItemClickListener.onItemClicked(mRecyclerView, v, holder.getAdapterPosition()); + } + } + }; + + private final OnLongClickListener mOnLongClickListener = new OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + if (mOnItemLongClickListener != null) { + ViewHolder holder = mRecyclerView.getChildViewHolder(v); + return mOnItemLongClickListener.onItemLongClicked( + mRecyclerView, v, holder.getAdapterPosition()); + } + return false; + } + }; + + private final RecyclerView.OnChildAttachStateChangeListener mAttachListener = + new RecyclerView.OnChildAttachStateChangeListener() { + @Override + public void onChildViewAttachedToWindow(View view) { + if (mOnItemClickListener != null) { + view.setOnClickListener(mOnClickListener); + } + if (mOnItemLongClickListener != null) { + view.setOnLongClickListener(mOnLongClickListener); + } + } + + @Override + public void onChildViewDetachedFromWindow(View view) {} + }; + + private ItemClickSupport(@NonNull RecyclerView recyclerView) { + mRecyclerView = recyclerView; + mRecyclerView.setTag(R.id.item_click_support, this); + mRecyclerView.addOnChildAttachStateChangeListener(mAttachListener); + } + + @NonNull + public static ItemClickSupport addTo(@NonNull RecyclerView view) { + ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support); + if (support == null) { + support = new ItemClickSupport(view); + } + return support; + } + + public static void removeFrom(@NonNull RecyclerView view) { + ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support); + if (support != null) { + support.detach(view); + } + } + + private void detach(@NonNull RecyclerView view) { + view.removeOnChildAttachStateChangeListener(mAttachListener); + view.setTag(R.id.item_click_support, null); + } + + /** + * Register a callback to be invoked when an item in the + * RecyclerView has been clicked. + * + * @param listener The callback that will be invoked. + */ + public void setOnItemClickListener(@Nullable OnItemClickListener listener) { + mOnItemClickListener = listener; + } + + /** + * Register a callback to be invoked when an item in the + * RecyclerView has been clicked and held. + * + * @param listener The callback that will be invoked. + */ + public void setOnItemLongClickListener(@Nullable OnItemLongClickListener listener) { + if (!mRecyclerView.isLongClickable()) { + mRecyclerView.setLongClickable(true); + } + mOnItemLongClickListener = listener; + } + + /** + * Interface definition for a callback to be invoked when an item in the + * RecyclerView has been clicked. + */ + public interface OnItemClickListener { + + /** + * Callback method to be invoked when an item in the RecyclerView + * has been clicked. + * + * @param recyclerView The RecyclerView where the click happened. + * @param view The view within the RecyclerView that was clicked + * (this will be a view provided by the adapter). + * @param position The position of the view in the adapter. + */ + void onItemClicked(RecyclerView recyclerView, View view, int position); + } + + /** + * Interface definition for a callback to be invoked when an item in the + * RecyclerView has been clicked and held. + */ + public interface OnItemLongClickListener { + + /** + * Callback method to be invoked when an item in the RecyclerView + * has been clicked and held. + * + * @param recyclerView The RecyclerView where the click happened. + * @param view The view within the RecyclerView that was clicked + * @param position The position of the view in the adapter. + * @return true if the callback consumed the long click, false otherwise + */ + boolean onItemLongClicked(RecyclerView recyclerView, View view, int position); + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_card.9.png b/app/src/main/res/drawable/bg_card.9.png new file mode 100755 index 0000000..27f67cd Binary files /dev/null and b/app/src/main/res/drawable/bg_card.9.png differ diff --git a/app/src/main/res/drawable/bottom_border.xml b/app/src/main/res/drawable/bottom_border.xml old mode 100644 new mode 100755 diff --git a/app/src/main/res/layout/activity_detail.xml b/app/src/main/res/layout/activity_detail.xml new file mode 100755 index 0000000..f99b5a0 --- /dev/null +++ b/app/src/main/res/layout/activity_detail.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml old mode 100644 new mode 100755 index 7463293..99a950c --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -6,29 +6,10 @@ android:layout_height="match_parent" android:fitsSystemWindows="true"> - - - - - - - + android:layout_height="match_parent"/> diff --git a/app/src/main/res/layout/app_bar_main.xml b/app/src/main/res/layout/app_bar_main.xml new file mode 100755 index 0000000..dbf3355 --- /dev/null +++ b/app/src/main/res/layout/app_bar_main.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml new file mode 100755 index 0000000..0dbbe5a --- /dev/null +++ b/app/src/main/res/layout/content_main.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_header.xml b/app/src/main/res/layout/drawer_header.xml old mode 100644 new mode 100755 index 42d4873..49ada9a --- a/app/src/main/res/layout/drawer_header.xml +++ b/app/src/main/res/layout/drawer_header.xml @@ -1,18 +1,16 @@ + android:theme="@style/ThemeOverlay.AppCompat.Dark"> + android:layout_centerInParent="true"/> diff --git a/app/src/main/res/layout/picture_list_item.xml b/app/src/main/res/layout/picture_list_item.xml old mode 100644 new mode 100755 index 6b5a158..d2fed32 --- a/app/src/main/res/layout/picture_list_item.xml +++ b/app/src/main/res/layout/picture_list_item.xml @@ -1,11 +1,34 @@ - + - + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/pictures_list_fragment.xml b/app/src/main/res/layout/pictures_list_fragment.xml old mode 100644 new mode 100755 index c0d3489..94ad6a5 --- a/app/src/main/res/layout/pictures_list_fragment.xml +++ b/app/src/main/res/layout/pictures_list_fragment.xml @@ -1,10 +1,26 @@ - - + android:layout_height="match_parent" + android:fitsSystemWindows="false" + android:orientation="vertical"> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_empty.xml b/app/src/main/res/layout/view_empty.xml new file mode 100755 index 0000000..4e1fe58 --- /dev/null +++ b/app/src/main/res/layout/view_empty.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_error.xml b/app/src/main/res/layout/view_error.xml new file mode 100755 index 0000000..84f30bf --- /dev/null +++ b/app/src/main/res/layout/view_error.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_loading.xml b/app/src/main/res/layout/view_loading.xml new file mode 100755 index 0000000..e772b3c --- /dev/null +++ b/app/src/main/res/layout/view_loading.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/welcome_fragment.xml b/app/src/main/res/layout/welcome_fragment.xml old mode 100644 new mode 100755 diff --git a/app/src/main/res/menu/menu_drawer.xml b/app/src/main/res/menu/menu_drawer.xml old mode 100644 new mode 100755 index 6c9bb6a..2687a01 --- a/app/src/main/res/menu/menu_drawer.xml +++ b/app/src/main/res/menu/menu_drawer.xml @@ -1,17 +1,15 @@ - + xmlns:tools="http://schemas.android.com/tools" + tools:context=".MainActivity"> + - + android:title="@string/now_showing_movies" /> + - - + + diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml old mode 100644 new mode 100755 diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png old mode 100644 new mode 100755 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png old mode 100644 new mode 100755 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png old mode 100644 new mode 100755 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png old mode 100644 new mode 100755 diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml new file mode 100755 index 0000000..48c20c5 --- /dev/null +++ b/app/src/main/res/values-land/dimens.xml @@ -0,0 +1,6 @@ + + + + 248dp + + diff --git a/app/src/main/res/values-land/integers.xml b/app/src/main/res/values-land/integers.xml new file mode 100755 index 0000000..934f847 --- /dev/null +++ b/app/src/main/res/values-land/integers.xml @@ -0,0 +1,4 @@ + + + 2 + \ No newline at end of file diff --git a/app/src/main/res/values-w820dp/dimens.xml b/app/src/main/res/values-w820dp/dimens.xml old mode 100644 new mode 100755 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml old mode 100644 new mode 100755 index 99d146e..69361c2 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -7,4 +7,11 @@ #212121 #f8f8f8 #727272 + + #363F44 + #ff1a1f23 + #E91C62 + #363F44 + + #ff4e5d64 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml old mode 100644 new mode 100755 index 47c8224..32d3602 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -2,4 +2,8 @@ 16dp 16dp + 160dp + 480dp + 8dp + 8dp diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml new file mode 100755 index 0000000..824c6e7 --- /dev/null +++ b/app/src/main/res/values/ids.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml new file mode 100755 index 0000000..cb52882 --- /dev/null +++ b/app/src/main/res/values/integers.xml @@ -0,0 +1,4 @@ + + + 1 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml old mode 100644 new mode 100755 index 63e232e..c4157a4 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,4 +3,10 @@ Hello world! Settings + No data + Upcoming Movies + Tagline + Description + Reviews + Now showing in theatres diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml old mode 100644 new mode 100755 index 8a599e8..a808d85 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -2,12 +2,32 @@ + + + + diff --git a/build.gradle b/build.gradle old mode 100644 new mode 100755 index 9405f3f..06dbc36 --- a/build.gradle +++ b/build.gradle @@ -1,11 +1,13 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. +apply from: "config.gradle" buildscript { repositories { jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:1.2.3' + classpath 'com.android.tools.build:gradle:1.5.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/config.gradle b/config.gradle new file mode 100755 index 0000000..aa1c298 --- /dev/null +++ b/config.gradle @@ -0,0 +1,33 @@ +def supportVersion = '23.1.1' +ext { + + android = [compileSdkVersion : 23, + buildToolsVersion : '23.0.2', + minSdkVersion : 14, + targetSdkVersion : 22, + versionCode : 20, + versionName : '2.0.0'] + + dependencies = [supportAppCompat : 'com.android.support:appcompat-v7:' + supportVersion, + supportV4 : 'com.android.support:support-v4:' + supportVersion, + supportDesign : 'com.android.support:design:' + supportVersion, + supportRecyclerview : 'com.android.support:recyclerview-v7:' + supportVersion, + supportAnnotations : 'com.android.support:support-annotations:' + supportVersion, + supportCardView : 'com.android.support:cardview-v7:' + supportVersion, + + picasso : 'com.squareup.picasso:picasso:2.5.2', + retrofit : 'com.squareup.retrofit:retrofit:2.0.0-beta2', + rfConverterSimpleXml: 'com.squareup.retrofit:converter-simplexml:2.0.0-beta2', + rfAdapterRxjava : 'com.squareup.retrofit:adapter-rxjava:2.0.0-beta2', + rxjava : 'io.reactivex:rxjava:1.0.16', + rxandroid : 'io.reactivex:rxandroid:1.0.1', + leakcanary : 'com.squareup.leakcanary:leakcanary-android:1.3.1', + junit : 'junit:junit:4.12', + mockito : 'org.mockito:mockito-core:1.10.19', + timber : 'com.jakewharton.timber:timber:4.1.0', + mosby : 'com.hannesdorfmann.mosby:mvp:2.0.0', + mosbyViewState : 'com.hannesdorfmann.mosby:viewstate:2.0.0', + dagger2 : 'com.google.dagger:dagger:2.0.1', + dagger2Compiler : 'com.google.dagger:dagger-compiler:2.0.1'] + +} \ No newline at end of file diff --git a/data/.gitignore b/data/.gitignore new file mode 100755 index 0000000..796b96d --- /dev/null +++ b/data/.gitignore @@ -0,0 +1 @@ +/build diff --git a/data/build.gradle b/data/build.gradle new file mode 100755 index 0000000..90fd3d6 --- /dev/null +++ b/data/build.gradle @@ -0,0 +1,58 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'me.tatarka:gradle-retrolambda:3.2.3' + //classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4' + } +} + +apply plugin: 'me.tatarka.retrolambda' +apply plugin: 'com.android.library' + +repositories { + maven { url "https://oss.sonatype.org/content/repositories/snapshots" } +} + +android { + compileSdkVersion rootProject.ext.android.compileSdkVersion + buildToolsVersion rootProject.ext.android.buildToolsVersion + + defaultConfig { + minSdkVersion rootProject.ext.android.minSdkVersion + targetSdkVersion rootProject.ext.android.targetSdkVersion + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + encoding "UTF-8" + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile project(':domain') + compile rootProject.ext.dependencies.retrofit + compile rootProject.ext.dependencies.rfAdapterRxjava + compile (rootProject.ext.dependencies.rfConverterSimpleXml) { + exclude module: 'stax-api' + exclude module: 'stax-stax' + exclude module: 'xpp3:xpp3' + } + compile 'au.com.gridstone.rxstore:rxstore:4.0.0' + compile 'au.com.gridstone.rxstore:converter-gson:4.0.0' + + testCompile rootProject.ext.dependencies.junit + testCompile rootProject.ext.dependencies.mockito + //testCompile 'org.robolectric:robolectric:3.0' +} diff --git a/data/proguard-rules.pro b/data/proguard-rules.pro new file mode 100755 index 0000000..95065db --- /dev/null +++ b/data/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in d:\Android\sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/app/src/androidTest/java/android/coding/interview/makeitawesome/ApplicationTest.java b/data/src/androidTest/java/android/coding/interview/makeitawesome/data/ApplicationTest.java old mode 100644 new mode 100755 similarity index 85% rename from app/src/androidTest/java/android/coding/interview/makeitawesome/ApplicationTest.java rename to data/src/androidTest/java/android/coding/interview/makeitawesome/data/ApplicationTest.java index df1596f..b65ec4f --- a/app/src/androidTest/java/android/coding/interview/makeitawesome/ApplicationTest.java +++ b/data/src/androidTest/java/android/coding/interview/makeitawesome/data/ApplicationTest.java @@ -1,4 +1,4 @@ -package android.coding.interview.makeitawesome; +package android.coding.interview.makeitawesome.data; import android.app.Application; import android.test.ApplicationTestCase; @@ -10,4 +10,4 @@ public class ApplicationTest extends ApplicationTestCase { public ApplicationTest() { super(Application.class); } -} +} \ No newline at end of file diff --git a/data/src/main/AndroidManifest.xml b/data/src/main/AndroidManifest.xml new file mode 100755 index 0000000..7a04aca --- /dev/null +++ b/data/src/main/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/data/src/main/java/android/coding/interview/makeitawesome/data/FinnkinoApi.java b/data/src/main/java/android/coding/interview/makeitawesome/data/FinnkinoApi.java new file mode 100755 index 0000000..e708f82 --- /dev/null +++ b/data/src/main/java/android/coding/interview/makeitawesome/data/FinnkinoApi.java @@ -0,0 +1,52 @@ +package android.coding.interview.makeitawesome.data; + +import android.coding.interview.makeitawesome.domain.entity.Events; + +import com.squareup.okhttp.OkHttpClient; + +import retrofit.Retrofit; +import retrofit.RxJavaCallAdapterFactory; +import retrofit.SimpleXmlConverterFactory; +import retrofit.http.GET; +import retrofit.http.Query; +import rx.Observable; + +/** + * REST API + * @see Finnkino Api + */ +public interface FinnkinoApi { + String BASE_URL = "http://www.finnkino.fi/xml/"; + + @GET("Events") + Observable getEvents(@Query("listType") String listType, @Query("area") int areaId); + + class Factory { + public static FinnkinoApi create(OkHttpClient client) { + Retrofit retrofit = new Retrofit.Builder().baseUrl(BASE_URL) + .client(client) + .addConverterFactory(SimpleXmlConverterFactory.create()) + .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) + .build(); + return retrofit.create(FinnkinoApi.class); + } + + public static FinnkinoApi create() { + Retrofit retrofit = new Retrofit.Builder().baseUrl(BASE_URL) + .addConverterFactory(SimpleXmlConverterFactory.create()) + .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) + .build(); + return retrofit.create(FinnkinoApi.class); + } + } + + class AreaID { + public static final int Helsinki = 1002; + public static final int Espoo = 1012; + } + + class ListType { + public static final String ComingSoon = "ComingSoon"; + public static final String NowInThreatres = "NowInTheatres"; + } +} diff --git a/data/src/main/java/android/coding/interview/makeitawesome/data/RepositoryImpl.java b/data/src/main/java/android/coding/interview/makeitawesome/data/RepositoryImpl.java new file mode 100755 index 0000000..daaaf32 --- /dev/null +++ b/data/src/main/java/android/coding/interview/makeitawesome/data/RepositoryImpl.java @@ -0,0 +1,40 @@ +package android.coding.interview.makeitawesome.data; + +import android.coding.interview.makeitawesome.domain.Repository; +import android.coding.interview.makeitawesome.domain.entity.Events; + +import javax.inject.Inject; + +import au.com.gridstone.rxstore.RxStore; +import rx.Observable; + +/** + * Simple implementation of repository pattern, it caches data fetched from Internet for "offline" usage + */ +public class RepositoryImpl implements Repository { + private final FinnkinoApi mFinnkinoApi; + private final RxStore rxStore; + + + @Inject + public RepositoryImpl(FinnkinoApi mFinnkinoApi, RxStore rxStore) { + this.mFinnkinoApi = mFinnkinoApi; + this.rxStore = rxStore; + } + + @Override + public Observable getUpcomingMovies() { + return mFinnkinoApi + .getEvents(FinnkinoApi.ListType.ComingSoon, FinnkinoApi.AreaID.Helsinki) + .flatMap(events -> rxStore.put(KEY_EVENTS_UPCOMING_MOVIES, events)) + .onErrorResumeNext(error -> rxStore.get(KEY_EVENTS_UPCOMING_MOVIES, Events.class)); + } + + @Override + public Observable getNowShowingMovies(int areaId) { + return mFinnkinoApi + .getEvents(FinnkinoApi.ListType.NowInThreatres, areaId) + .flatMap(events -> rxStore.put(KEY_EVENTS_NOW_IN_THEATRES, events)) + .onErrorResumeNext(error -> rxStore.get(KEY_EVENTS_NOW_IN_THEATRES, Events.class)); + } +} diff --git a/data/src/main/res/values/strings.xml b/data/src/main/res/values/strings.xml new file mode 100755 index 0000000..e4ffffe --- /dev/null +++ b/data/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Data + diff --git a/data/src/test/java/android/coding/interview/makeitawesome/data/RepositoryImplTest.java b/data/src/test/java/android/coding/interview/makeitawesome/data/RepositoryImplTest.java new file mode 100755 index 0000000..fb6d06a --- /dev/null +++ b/data/src/test/java/android/coding/interview/makeitawesome/data/RepositoryImplTest.java @@ -0,0 +1,52 @@ +package android.coding.interview.makeitawesome.data; + +import android.coding.interview.makeitawesome.domain.Repository; +import android.coding.interview.makeitawesome.domain.entity.Events; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import au.com.gridstone.rxstore.RxStore; +import rx.Observable; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Created by Liang on 2016/1/31. + */ +/*@RunWith(RobolectricGradleTestRunner.class) +@Config(constants = BuildConfig.class, sdk = 21)*/ +public class RepositoryImplTest { + + @Mock + FinnkinoApi finnkinoApi; + + @Mock + RxStore rxStore; + + Repository repository; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + repository = new RepositoryImpl(finnkinoApi, rxStore); + } + + @Test + public void testGetUpcomingMovies() throws Exception { + when(finnkinoApi.getEvents(FinnkinoApi.ListType.ComingSoon, FinnkinoApi.AreaID.Helsinki)).thenReturn(Observable.empty()); + repository.getUpcomingMovies(); + verify(finnkinoApi, times(1)).getEvents(FinnkinoApi.ListType.ComingSoon, FinnkinoApi.AreaID.Helsinki); + } + + @Test + public void testGetNowShowingMovies() throws Exception { + when(finnkinoApi.getEvents(FinnkinoApi.ListType.NowInThreatres, FinnkinoApi.AreaID.Helsinki)).thenReturn(Observable.empty()); + repository.getNowShowingMovies(FinnkinoApi.AreaID.Helsinki); + verify(finnkinoApi, times(1)).getEvents(FinnkinoApi.ListType.NowInThreatres, FinnkinoApi.AreaID.Helsinki); + } +} \ No newline at end of file diff --git a/domain/.gitignore b/domain/.gitignore new file mode 100755 index 0000000..796b96d --- /dev/null +++ b/domain/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/build.gradle b/domain/build.gradle new file mode 100755 index 0000000..b18bd4d --- /dev/null +++ b/domain/build.gradle @@ -0,0 +1,20 @@ +apply plugin: 'java' + +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + + compile rootProject.ext.dependencies.rxjava + compile 'javax.inject:javax.inject:1' + + compile ('org.simpleframework:simple-xml:2.7.1') { + exclude module: 'stax-api' + exclude module: 'stax-stax' + exclude module: 'xpp3:xpp3' + } + + testCompile rootProject.ext.dependencies.junit + testCompile rootProject.ext.dependencies.mockito +} \ No newline at end of file diff --git a/domain/src/main/java/android/coding/interview/makeitawesome/domain/Repository.java b/domain/src/main/java/android/coding/interview/makeitawesome/domain/Repository.java new file mode 100755 index 0000000..0db5f2e --- /dev/null +++ b/domain/src/main/java/android/coding/interview/makeitawesome/domain/Repository.java @@ -0,0 +1,17 @@ +package android.coding.interview.makeitawesome.domain; + +import android.coding.interview.makeitawesome.domain.entity.Events; + +import rx.Observable; + +/** + * Interface for implementing repository pattern + */ +public interface Repository { + String KEY_EVENTS_UPCOMING_MOVIES = "upcoming_movies"; + String KEY_EVENTS_NOW_IN_THEATRES = "now_in_theatres"; + + Observable getUpcomingMovies(); + + Observable getNowShowingMovies(int areaId); +} diff --git a/domain/src/main/java/android/coding/interview/makeitawesome/domain/entity/Actor.java b/domain/src/main/java/android/coding/interview/makeitawesome/domain/entity/Actor.java new file mode 100755 index 0000000..e9ab9d3 --- /dev/null +++ b/domain/src/main/java/android/coding/interview/makeitawesome/domain/entity/Actor.java @@ -0,0 +1,37 @@ +package android.coding.interview.makeitawesome.domain.entity; + +import org.simpleframework.xml.Element; +import org.simpleframework.xml.Root; + +/** + * Created by Liang on 2016/1/30. + */ +@Root +public class Actor { + @Element(required = false) + private String FirstName; + + @Element(required = false) + private String LastName; + + public String getFirstName() { + return FirstName; + } + + public void setFirstName(String FirstName) { + this.FirstName = FirstName; + } + + public String getLastName() { + return LastName; + } + + public void setLastName(String LastName) { + this.LastName = LastName; + } + + @Override + public String toString() { + return "Actor [FirstName = " + FirstName + ", LastName = " + LastName + "]"; + } +} diff --git a/domain/src/main/java/android/coding/interview/makeitawesome/domain/entity/ContentDescriptor.java b/domain/src/main/java/android/coding/interview/makeitawesome/domain/entity/ContentDescriptor.java new file mode 100755 index 0000000..5ac6ef0 --- /dev/null +++ b/domain/src/main/java/android/coding/interview/makeitawesome/domain/entity/ContentDescriptor.java @@ -0,0 +1,41 @@ +package android.coding.interview.makeitawesome.domain.entity; + +import org.simpleframework.xml.Element; +import org.simpleframework.xml.Root; + +/** + * Created by Liang on 2016/1/30. + */ +@Root +public class ContentDescriptor { + @Element(required = false) + private String Name; + @Element(required = false) + private String ImageURL; + + public String getName () + { + return Name; + } + + public void setName (String Name) + { + this.Name = Name; + } + + public String getImageURL () + { + return ImageURL; + } + + public void setImageURL (String ImageURL) + { + this.ImageURL = ImageURL; + } + + @Override + public String toString() + { + return "ContentDescriptor [Name = "+Name+", ImageURL = "+ImageURL+"]"; + } +} diff --git a/domain/src/main/java/android/coding/interview/makeitawesome/domain/entity/Director.java b/domain/src/main/java/android/coding/interview/makeitawesome/domain/entity/Director.java new file mode 100755 index 0000000..abc3bfd --- /dev/null +++ b/domain/src/main/java/android/coding/interview/makeitawesome/domain/entity/Director.java @@ -0,0 +1,36 @@ +package android.coding.interview.makeitawesome.domain.entity; + +import org.simpleframework.xml.Element; +import org.simpleframework.xml.Root; + +/** + * Created by Liang on 2016/1/30. + */ +@Root +public class Director { + @Element(required = false) + private String FirstName; + @Element(required = false) + private String LastName; + + public String getFirstName() { + return FirstName; + } + + public void setFirstName(String FirstName) { + this.FirstName = FirstName; + } + + public String getLastName() { + return LastName; + } + + public void setLastName(String LastName) { + this.LastName = LastName; + } + + @Override + public String toString() { + return "Director [FirstName = " + FirstName + ", LastName = " + LastName + "]"; + } +} diff --git a/domain/src/main/java/android/coding/interview/makeitawesome/domain/entity/Event.java b/domain/src/main/java/android/coding/interview/makeitawesome/domain/entity/Event.java new file mode 100755 index 0000000..7eea0b4 --- /dev/null +++ b/domain/src/main/java/android/coding/interview/makeitawesome/domain/entity/Event.java @@ -0,0 +1,239 @@ +package android.coding.interview.makeitawesome.domain.entity; + +import org.simpleframework.xml.Element; +import org.simpleframework.xml.ElementList; +import org.simpleframework.xml.Root; + +import java.util.List; + +/** + * Created by Liang on 2016/1/30. + */ +@Root(name = "Event") +public class Event { + @Element(required = false) + private String RatingLabel; + @Element(required = false) + private String EventURL; + @Element(required = false) + private String RatingImageUrl; + @Element(required = false) + private Images Images; + @Element(required = false) + private String EventType; + @Element(required = false) + private String Title; + @Element(required = false) + private String dtLocalRelease; + @ElementList(required = false) + private List ContentDescriptors; + @Element(required = false) + private String LengthInMinutes; + @Element(required = false) + private String GlobalDistributorName; + @ElementList(required = false) + private List Directors; + @Element(required = false) + private String Rating; + @ElementList(required = false) + private List Videos; + @Element(required = false) + private String OriginalTitle; + @ElementList(required = false) + private List Cast; + @Element(required = false) + private String ID; + @Element(required = false) + private String Genres; + @Element(required = false) + private String ShortSynopsis; + @Element(required = false) + private String Synopsis; + @Element(required = false) + private String ProductionCompanies; + @Element(required = false) + private String LocalDistributorName; + @Element(required = false) + private String ProductionYear; + + public String getRatingLabel() { + return RatingLabel; + } + + public void setRatingLabel(String RatingLabel) { + this.RatingLabel = RatingLabel; + } + + public String getEventURL() { + return EventURL; + } + + public void setEventURL(String EventURL) { + this.EventURL = EventURL; + } + + public String getRatingImageUrl() { + return RatingImageUrl; + } + + public void setRatingImageUrl(String RatingImageUrl) { + this.RatingImageUrl = RatingImageUrl; + } + + public Images getImages() { + return Images; + } + + public void setImages(Images Images) { + this.Images = Images; + } + + public String getEventType() { + return EventType; + } + + public void setEventType(String EventType) { + this.EventType = EventType; + } + + public String getTitle() { + return Title; + } + + public void setTitle(String Title) { + this.Title = Title; + } + + public String getDtLocalRelease() { + return dtLocalRelease; + } + + public void setDtLocalRelease(String dtLocalRelease) { + this.dtLocalRelease = dtLocalRelease; + } + + public List getContentDescriptors() { + return ContentDescriptors; + } + + public void setContentDescriptors(List ContentDescriptors) { + this.ContentDescriptors = ContentDescriptors; + } + + public String getLengthInMinutes() { + return LengthInMinutes; + } + + public void setLengthInMinutes(String LengthInMinutes) { + this.LengthInMinutes = LengthInMinutes; + } + + public String getGlobalDistributorName() { + return GlobalDistributorName; + } + + public void setGlobalDistributorName(String GlobalDistributorName) { + this.GlobalDistributorName = GlobalDistributorName; + } + + public List getDirectors() { + return Directors; + } + + public void setDirectors(List Directors) { + this.Directors = Directors; + } + + public String getRating() { + return Rating; + } + + public void setRating(String Rating) { + this.Rating = Rating; + } + + public List getVideos() { + return Videos; + } + + public void setVideos(List Videos) { + this.Videos = Videos; + } + + public String getOriginalTitle() { + return OriginalTitle; + } + + public void setOriginalTitle(String OriginalTitle) { + this.OriginalTitle = OriginalTitle; + } + + public List getCast() { + return Cast; + } + + public void setCast(List Cast) { + this.Cast = Cast; + } + + public String getID() { + return ID; + } + + public void setID(String ID) { + this.ID = ID; + } + + public String getGenres() { + return Genres; + } + + public void setGenres(String Genres) { + this.Genres = Genres; + } + + public String getShortSynopsis() { + return ShortSynopsis; + } + + public void setShortSynopsis(String ShortSynopsis) { + this.ShortSynopsis = ShortSynopsis; + } + + public String getSynopsis() { + return Synopsis; + } + + public void setSynopsis(String Synopsis) { + this.Synopsis = Synopsis; + } + + public String getProductionCompanies() { + return ProductionCompanies; + } + + public void setProductionCompanies(String ProductionCompanies) { + this.ProductionCompanies = ProductionCompanies; + } + + public String getLocalDistributorName() { + return LocalDistributorName; + } + + public void setLocalDistributorName(String LocalDistributorName) { + this.LocalDistributorName = LocalDistributorName; + } + + public String getProductionYear() { + return ProductionYear; + } + + public void setProductionYear(String ProductionYear) { + this.ProductionYear = ProductionYear; + } + + @Override + public String toString() { + return "Event [RatingLabel = " + RatingLabel + ", EventURL = " + EventURL + ", RatingImageUrl = " + RatingImageUrl + ", Images = " + Images + ", EventType = " + EventType + ", Title = " + Title + ", dtLocalRelease = " + dtLocalRelease + ", ContentDescriptors = " + ContentDescriptors + ", LengthInMinutes = " + LengthInMinutes + ", GlobalDistributorName = " + GlobalDistributorName + ", Directors = " + Directors + ", Rating = " + Rating + ", Videos = " + Videos + ", OriginalTitle = " + OriginalTitle + ", Cast = " + Cast + ", ID = " + ID + ", Genres = " + Genres + ", ShortSynopsis = " + ShortSynopsis + ", Synopsis = " + Synopsis + ", ProductionCompanies = " + ProductionCompanies + ", LocalDistributorName = " + LocalDistributorName + ", ProductionYear = " + ProductionYear + "]"; + } +} diff --git a/domain/src/main/java/android/coding/interview/makeitawesome/domain/entity/EventVideo.java b/domain/src/main/java/android/coding/interview/makeitawesome/domain/entity/EventVideo.java new file mode 100755 index 0000000..ce9b0b8 --- /dev/null +++ b/domain/src/main/java/android/coding/interview/makeitawesome/domain/entity/EventVideo.java @@ -0,0 +1,72 @@ +package android.coding.interview.makeitawesome.domain.entity; + +import org.simpleframework.xml.Element; +import org.simpleframework.xml.Root; + +/** + * Created by Liang on 2016/1/30. + */ +@Root +public class EventVideo { + + @Element(required = false) + private String ThumbnailLocation; + + @Element(required = false) + private String MediaResourceSubType; + + @Element(required = false) + private String Location; + + @Element(required = false) + private String Title; + + @Element(required = false) + private String MediaResourceFormat; + + public String getThumbnailLocation() { + return ThumbnailLocation; + } + + public void setThumbnailLocation(String ThumbnailLocation) { + this.ThumbnailLocation = ThumbnailLocation; + } + + public String getMediaResourceSubType() { + return MediaResourceSubType; + } + + public void setMediaResourceSubType(String MediaResourceSubType) { + this.MediaResourceSubType = MediaResourceSubType; + } + + public String getLocation() { + return Location; + } + + public void setLocation(String Location) { + this.Location = Location; + } + + public String getTitle() { + return Title; + } + + public void setTitle(String Title) { + this.Title = Title; + } + + public String getMediaResourceFormat() { + return MediaResourceFormat; + } + + public void setMediaResourceFormat(String MediaResourceFormat) { + this.MediaResourceFormat = MediaResourceFormat; + } + + @Override + public String toString() { + return "EventVideo [ThumbnailLocation = " + ThumbnailLocation + ", MediaResourceSubType = " + MediaResourceSubType + ", Location = " + Location + ", Title = " + Title + ", MediaResourceFormat = " + MediaResourceFormat + "]"; + } +} + diff --git a/domain/src/main/java/android/coding/interview/makeitawesome/domain/entity/Events.java b/domain/src/main/java/android/coding/interview/makeitawesome/domain/entity/Events.java new file mode 100755 index 0000000..2cd4c75 --- /dev/null +++ b/domain/src/main/java/android/coding/interview/makeitawesome/domain/entity/Events.java @@ -0,0 +1,23 @@ +package android.coding.interview.makeitawesome.domain.entity; + +import org.simpleframework.xml.ElementList; +import org.simpleframework.xml.Root; + +import java.util.List; + +/** + * Created by Liang on 2016/1/30. + */ +@Root +public class Events { + @ElementList(inline=true) + private List events; + + public List getEvents() { + return events; + } + + public void setEvents(List events) { + this.events = events; + } +} diff --git a/domain/src/main/java/android/coding/interview/makeitawesome/domain/entity/Images.java b/domain/src/main/java/android/coding/interview/makeitawesome/domain/entity/Images.java new file mode 100755 index 0000000..1868f60 --- /dev/null +++ b/domain/src/main/java/android/coding/interview/makeitawesome/domain/entity/Images.java @@ -0,0 +1,89 @@ +package android.coding.interview.makeitawesome.domain.entity; + +import org.simpleframework.xml.Element; +import org.simpleframework.xml.Root; + +/** + * Created by Liang on 2016/1/30. + */ +@Root +public class Images { + @Element(required = false) + private String EventSmallImagePortrait; + @Element(required = false) + private String EventLargeImageLandscape; + @Element(required = false) + private String EventMediumImagePortrait; + @Element(required = false) + private String EventSmallImageLandscape; + @Element(required = false) + private String EventMicroImagePortrait; + @Element(required = false) + private String EventLargeImagePortrait; + + public String getEventSmallImagePortrait () + { + return EventSmallImagePortrait; + } + + public void setEventSmallImagePortrait (String EventSmallImagePortrait) + { + this.EventSmallImagePortrait = EventSmallImagePortrait; + } + + public String getEventLargeImageLandscape () + { + return EventLargeImageLandscape; + } + + public void setEventLargeImageLandscape (String EventLargeImageLandscape) + { + this.EventLargeImageLandscape = EventLargeImageLandscape; + } + + public String getEventMediumImagePortrait () + { + return EventMediumImagePortrait; + } + + public void setEventMediumImagePortrait (String EventMediumImagePortrait) + { + this.EventMediumImagePortrait = EventMediumImagePortrait; + } + + public String getEventSmallImageLandscape () + { + return EventSmallImageLandscape; + } + + public void setEventSmallImageLandscape (String EventSmallImageLandscape) + { + this.EventSmallImageLandscape = EventSmallImageLandscape; + } + + public String getEventMicroImagePortrait () + { + return EventMicroImagePortrait; + } + + public void setEventMicroImagePortrait (String EventMicroImagePortrait) + { + this.EventMicroImagePortrait = EventMicroImagePortrait; + } + + public String getEventLargeImagePortrait () + { + return EventLargeImagePortrait; + } + + public void setEventLargeImagePortrait (String EventLargeImagePortrait) + { + this.EventLargeImagePortrait = EventLargeImagePortrait; + } + + @Override + public String toString() + { + return "Images [EventSmallImagePortrait = "+EventSmallImagePortrait+", EventLargeImageLandscape = "+EventLargeImageLandscape+", EventMediumImagePortrait = "+EventMediumImagePortrait+", EventSmallImageLandscape = "+EventSmallImageLandscape+", EventMicroImagePortrait = "+EventMicroImagePortrait+", EventLargeImagePortrait = "+EventLargeImagePortrait+"]"; + } +} diff --git a/domain/src/main/java/android/coding/interview/makeitawesome/domain/interactor/BaseUseCaseImpl.java b/domain/src/main/java/android/coding/interview/makeitawesome/domain/interactor/BaseUseCaseImpl.java new file mode 100755 index 0000000..005050a --- /dev/null +++ b/domain/src/main/java/android/coding/interview/makeitawesome/domain/interactor/BaseUseCaseImpl.java @@ -0,0 +1,14 @@ +package android.coding.interview.makeitawesome.domain.interactor; + +import android.coding.interview.makeitawesome.domain.Repository; + +/** + * Created by Liang on 2016/1/30. + */ +public class BaseUseCaseImpl { + protected final Repository mRepository; + + public BaseUseCaseImpl(Repository mRepository) { + this.mRepository = mRepository; + } +} diff --git a/domain/src/main/java/android/coding/interview/makeitawesome/domain/interactor/GetNowShowingMoviesUseCase.java b/domain/src/main/java/android/coding/interview/makeitawesome/domain/interactor/GetNowShowingMoviesUseCase.java new file mode 100755 index 0000000..81313f1 --- /dev/null +++ b/domain/src/main/java/android/coding/interview/makeitawesome/domain/interactor/GetNowShowingMoviesUseCase.java @@ -0,0 +1,19 @@ +package android.coding.interview.makeitawesome.domain.interactor; + +import android.coding.interview.makeitawesome.domain.entity.Events; + +import rx.Observable; + +/** + * Interface to get movies are now showing in the theatres + *

+ * Created by Liang on 2016/1/30. + */ +public interface GetNowShowingMoviesUseCase { + /** + * @param areaId Theatre Area ID + * @return NowInTheatres Events Observable + * @see Finnkino Api + */ + Observable execute(int areaId); +} diff --git a/domain/src/main/java/android/coding/interview/makeitawesome/domain/interactor/GetNowShowingMoviesUseCaseImpl.java b/domain/src/main/java/android/coding/interview/makeitawesome/domain/interactor/GetNowShowingMoviesUseCaseImpl.java new file mode 100755 index 0000000..ac2b3d0 --- /dev/null +++ b/domain/src/main/java/android/coding/interview/makeitawesome/domain/interactor/GetNowShowingMoviesUseCaseImpl.java @@ -0,0 +1,24 @@ +package android.coding.interview.makeitawesome.domain.interactor; + +import android.coding.interview.makeitawesome.domain.Repository; +import android.coding.interview.makeitawesome.domain.entity.Events; + +import javax.inject.Inject; + +import rx.Observable; + +/** + * Created by Liang on 2016/1/30. + */ +public class GetNowShowingMoviesUseCaseImpl extends BaseUseCaseImpl implements GetNowShowingMoviesUseCase { + + @Inject + public GetNowShowingMoviesUseCaseImpl(Repository mRepository) { + super(mRepository); + } + + @Override + public Observable execute(int areaId) { + return mRepository.getNowShowingMovies(areaId); + } +} diff --git a/domain/src/main/java/android/coding/interview/makeitawesome/domain/interactor/GetUpcomingMoviesUseCase.java b/domain/src/main/java/android/coding/interview/makeitawesome/domain/interactor/GetUpcomingMoviesUseCase.java new file mode 100755 index 0000000..3b376a7 --- /dev/null +++ b/domain/src/main/java/android/coding/interview/makeitawesome/domain/interactor/GetUpcomingMoviesUseCase.java @@ -0,0 +1,17 @@ +package android.coding.interview.makeitawesome.domain.interactor; + +import android.coding.interview.makeitawesome.domain.entity.Events; + +import rx.Observable; + + +/** + * Interface to get upcoming movie events + */ +public interface GetUpcomingMoviesUseCase { + /** + * @return ComingSoon Events Observable + * @see Finnkino Api + */ + Observable execute(); +} diff --git a/domain/src/main/java/android/coding/interview/makeitawesome/domain/interactor/GetUpcomingMoviesUseCaseImpl.java b/domain/src/main/java/android/coding/interview/makeitawesome/domain/interactor/GetUpcomingMoviesUseCaseImpl.java new file mode 100755 index 0000000..6dc6375 --- /dev/null +++ b/domain/src/main/java/android/coding/interview/makeitawesome/domain/interactor/GetUpcomingMoviesUseCaseImpl.java @@ -0,0 +1,25 @@ +package android.coding.interview.makeitawesome.domain.interactor; + +import android.coding.interview.makeitawesome.domain.Repository; +import android.coding.interview.makeitawesome.domain.entity.Events; + +import javax.inject.Inject; + +import rx.Observable; + +/** + * Created by Liang on 2016/1/30. + */ +public class GetUpcomingMoviesUseCaseImpl extends BaseUseCaseImpl implements GetUpcomingMoviesUseCase { + + + @Inject + public GetUpcomingMoviesUseCaseImpl(Repository repository) { + super(repository); + } + + @Override + public Observable execute() { + return mRepository.getUpcomingMovies(); + } +} diff --git a/domain/src/test/java/android/coding/interview/makeitawesome/domain/interactor/BaseUseCaseImplTest.java b/domain/src/test/java/android/coding/interview/makeitawesome/domain/interactor/BaseUseCaseImplTest.java new file mode 100755 index 0000000..a46c9aa --- /dev/null +++ b/domain/src/test/java/android/coding/interview/makeitawesome/domain/interactor/BaseUseCaseImplTest.java @@ -0,0 +1,20 @@ +package android.coding.interview.makeitawesome.domain.interactor; + +import android.coding.interview.makeitawesome.domain.Repository; + +import org.junit.Before; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Created by Liang on 2016/1/31. + */ +public class BaseUseCaseImplTest { + @Mock + Repository repository; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + } +} diff --git a/domain/src/test/java/android/coding/interview/makeitawesome/domain/interactor/GetNowShowingMoviesUseCaseImplTest.java b/domain/src/test/java/android/coding/interview/makeitawesome/domain/interactor/GetNowShowingMoviesUseCaseImplTest.java new file mode 100755 index 0000000..abf0ab6 --- /dev/null +++ b/domain/src/test/java/android/coding/interview/makeitawesome/domain/interactor/GetNowShowingMoviesUseCaseImplTest.java @@ -0,0 +1,28 @@ +package android.coding.interview.makeitawesome.domain.interactor; + +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Created by Liang on 2016/1/31. + */ +public class GetNowShowingMoviesUseCaseImplTest extends BaseUseCaseImplTest{ + + GetNowShowingMoviesUseCase getNowShowingMoviesUseCase; + + @Before + public void setUp() throws Exception { + super.setUp(); + getNowShowingMoviesUseCase = new GetNowShowingMoviesUseCaseImpl(repository); + } + + @Test + public void testExecute() throws Exception { + getNowShowingMoviesUseCase.execute(anyInt()); + verify(repository, times(1)).getNowShowingMovies(anyInt()); + } +} \ No newline at end of file diff --git a/domain/src/test/java/android/coding/interview/makeitawesome/domain/interactor/GetUpcomingMoviesUseCaseImplTest.java b/domain/src/test/java/android/coding/interview/makeitawesome/domain/interactor/GetUpcomingMoviesUseCaseImplTest.java new file mode 100755 index 0000000..1b5c686 --- /dev/null +++ b/domain/src/test/java/android/coding/interview/makeitawesome/domain/interactor/GetUpcomingMoviesUseCaseImplTest.java @@ -0,0 +1,27 @@ +package android.coding.interview.makeitawesome.domain.interactor; + +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Created by Liang on 2016/1/31. + */ +public class GetUpcomingMoviesUseCaseImplTest extends BaseUseCaseImplTest{ + + GetUpcomingMoviesUseCase getUpcomingMoviesUseCase; + + @Before + public void setUp() throws Exception { + super.setUp(); + getUpcomingMoviesUseCase = new GetUpcomingMoviesUseCaseImpl(repository); + } + + @Test + public void testExecute() throws Exception { + getUpcomingMoviesUseCase.execute(); + verify(repository, times(1)).getUpcomingMovies(); + } +} \ No newline at end of file diff --git a/finnkino.json b/finnkino.json new file mode 100755 index 0000000..9572826 --- /dev/null +++ b/finnkino.json @@ -0,0 +1,59 @@ +{ + "ID": "301317", + "Title": "Zoolander 2", + "OriginalTitle": "Zoolander 2", + "ProductionYear": "2016", + "LengthInMinutes": "0", + "dtLocalRelease": "2016-02-12T00:00:00", + "Rating": "Tulossa", + "RatingLabel": "Tulossa", + "RatingImageUrl": "https://media.finnkino.fi/images/rating_large_Tulossa.png", + "LocalDistributorName": "Finnkinon teatterilevitys", + "GlobalDistributorName": "Finnkinon teatterilevitys - Paramount", + "ProductionCompanies": "-", + "EventType": "Movie", + "Genres": "Komedia", + "ShortSynopsis": "Maailman kuumimmat miesmallit Derek Zoolander (Ben Stiller) ja Hansel McDonald (Owen Wilson) palaavat valkokankaille Stillerin ohjaamassa kulttikomedian jatko-osassa.", + "Synopsis": "Maailman kuumimmat miesmallit Derek Zoolander (Ben Stiller) ja Hansel McDonald (Owen Wilson) palaavat valkokankaille Stillerin ohjaamassa kulttikomedian jatko-osassa.", + "EventURL": "http://www.finnkino.fi/Event/301317/", + "Images": { + "EventMicroImagePortrait": "http://media.finnkino.fi/1012/Event_10655/portrait_micro/Zoolander2_1080t.jpg", + "EventSmallImagePortrait": "http://media.finnkino.fi/1012/Event_10655/portrait_small/Zoolander2_1080t.jpg", + "EventMediumImagePortrait": "http://media.finnkino.fi/1012/Event_10655/portrait_medium/Zoolander2_1080t.jpg", + "EventLargeImagePortrait": "http://media.finnkino.fi/1012/Event_10655/portrait_small/Zoolander2_1080t.jpg", + "EventSmallImageLandscape": "http://media.finnkino.fi/1012/Event_10655/landscape_small/Zoolander2_444.jpg", + "EventLargeImageLandscape": "http://media.finnkino.fi/1012/Event_10655/landscape_large/Zoolander2_670.jpg" + }, + "Videos": " ", + "Cast": { + "Actor": [ + { + "FirstName": "Ben", + "LastName": "Stiller" + }, + { + "FirstName": "Owen", + "LastName": "Wilson" + }, + { + "FirstName": "Will", + "LastName": "Ferrell" + }, + { + "FirstName": "Penelope", + "LastName": "Cruz" + }, + { + "FirstName": "Kristen", + "LastName": "Wiig" + } + ] + }, + "Directors": { + "Director": { + "FirstName": "Ben", + "LastName": "Stiller" + } + }, + "ContentDescriptors": "" +} diff --git a/gradle.properties b/gradle.properties old mode 100644 new mode 100755 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties old mode 100644 new mode 100755 index 0c71e76..9f6a1ea --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Apr 10 15:27:10 PDT 2013 +#Sat Jan 30 11:00:48 EET 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip diff --git a/gradlew.bat b/gradlew.bat old mode 100644 new mode 100755 diff --git a/settings.gradle b/settings.gradle old mode 100644 new mode 100755 index e7b4def..5f094eb --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app' +include ':app', ':domain', ':data'