├── .firebaserc ├── .gitignore ├── .idea ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── deploymentTargetDropDown.xml ├── gradle.xml ├── jarRepositories.xml ├── misc.xml └── vcs.xml ├── README.md ├── Task ├── There ├── app ├── .gitignore ├── build.gradle ├── google-services.json ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── codingwithmitch │ │ └── cleannotes │ │ ├── BaseTest.kt │ │ ├── InstrumentationTestSuite.kt │ │ ├── di │ │ ├── TestAppComponent.kt │ │ ├── TestModule.kt │ │ └── TestNoteFragmentFactoryModule.kt │ │ ├── framework │ │ ├── MockTestRunner.kt │ │ ├── datasource │ │ │ ├── cache │ │ │ │ └── NoteDaoServiceTests.kt │ │ │ ├── data │ │ │ │ └── NoteDataFactory.kt │ │ │ └── network │ │ │ │ └── NoteFirestoreServiceTests.kt │ │ └── presentation │ │ │ ├── TestBaseApplication.kt │ │ │ ├── TestNoteFragmentFactory.kt │ │ │ ├── end_to_end │ │ │ └── NotesFeatureTest.kt │ │ │ ├── notedetail │ │ │ └── NoteDetailFragmentTests.kt │ │ │ └── notelist │ │ │ └── NoteListFragmentTests.kt │ │ └── util │ │ ├── EspressoIdlingResourceRule.kt │ │ └── ViewShownIdlingResource.kt │ ├── debug │ ├── assets │ │ └── note_list.json │ └── java │ │ └── com │ │ └── codingwithmitch │ │ └── cleannotes │ │ └── util │ │ └── EspressoIdlingResource.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── codingwithmitch │ │ │ └── cleannotes │ │ │ ├── business │ │ │ ├── data │ │ │ │ ├── cache │ │ │ │ │ ├── CacheConstants.kt │ │ │ │ │ ├── CacheErrors.kt │ │ │ │ │ ├── CacheResponseHandler.kt │ │ │ │ │ ├── CacheResult.kt │ │ │ │ │ ├── abstraction │ │ │ │ │ │ └── NoteCacheDataSource.kt │ │ │ │ │ └── implementation │ │ │ │ │ │ └── NoteCacheDataSourceImpl.kt │ │ │ │ ├── network │ │ │ │ │ ├── ApiResponseHandler.kt │ │ │ │ │ ├── ApiResult.kt │ │ │ │ │ ├── NetworkConstants.kt │ │ │ │ │ ├── NetworkErrors.kt │ │ │ │ │ ├── abstraction │ │ │ │ │ │ └── NoteNetworkDataSource.kt │ │ │ │ │ └── implementation │ │ │ │ │ │ └── NoteNetworkDataSourceImpl.kt │ │ │ │ └── util │ │ │ │ │ ├── GenericErrors.kt │ │ │ │ │ └── RepositoryExtensions.kt │ │ │ ├── domain │ │ │ │ ├── model │ │ │ │ │ ├── Note.kt │ │ │ │ │ └── NoteFactory.kt │ │ │ │ ├── state │ │ │ │ │ ├── DataChannelManager.kt │ │ │ │ │ ├── DataState.kt │ │ │ │ │ ├── MessageStack.kt │ │ │ │ │ ├── StateEvent.kt │ │ │ │ │ ├── StateEventManager.kt │ │ │ │ │ ├── StateResource.kt │ │ │ │ │ └── ViewState.kt │ │ │ │ └── util │ │ │ │ │ ├── DateUtil.kt │ │ │ │ │ └── EntityMapper.kt │ │ │ └── interactors │ │ │ │ ├── common │ │ │ │ └── DeleteNote.kt │ │ │ │ ├── notedetail │ │ │ │ ├── NoteDetailInteractors.kt │ │ │ │ └── UpdateNote.kt │ │ │ │ ├── notelist │ │ │ │ ├── DeleteMultipleNotes.kt │ │ │ │ ├── GetNumNotes.kt │ │ │ │ ├── InsertMultipleNotes.kt │ │ │ │ ├── InsertNewNote.kt │ │ │ │ ├── NoteListInteractors.kt │ │ │ │ ├── RestoreDeletedNote.kt │ │ │ │ └── SearchNotes.kt │ │ │ │ └── splash │ │ │ │ ├── SyncDeletedNotes.kt │ │ │ │ └── SyncNotes.kt │ │ │ ├── di │ │ │ ├── AppComponent.kt │ │ │ ├── AppModule.kt │ │ │ ├── NoteFragmentFactoryModule.kt │ │ │ ├── NoteViewModelModule.kt │ │ │ └── ProductionModule.kt │ │ │ ├── framework │ │ │ ├── datasource │ │ │ │ ├── cache │ │ │ │ │ ├── abstraction │ │ │ │ │ │ └── NoteDaoService.kt │ │ │ │ │ ├── database │ │ │ │ │ │ ├── NoteDao.kt │ │ │ │ │ │ └── NoteDatabase.kt │ │ │ │ │ ├── implementation │ │ │ │ │ │ └── NoteDaoServiceImpl.kt │ │ │ │ │ ├── mappers │ │ │ │ │ │ └── CacheMapper.kt │ │ │ │ │ └── model │ │ │ │ │ │ └── NoteCacheEntity.kt │ │ │ │ ├── network │ │ │ │ │ ├── abstraction │ │ │ │ │ │ └── NoteFirestoreService.kt │ │ │ │ │ ├── implementation │ │ │ │ │ │ └── NoteFirestoreServiceImpl.kt │ │ │ │ │ ├── mappers │ │ │ │ │ │ └── NetworkMapper.kt │ │ │ │ │ └── model │ │ │ │ │ │ └── NoteNetworkEntity.kt │ │ │ │ └── preferences │ │ │ │ │ └── PreferenceKeys.kt │ │ │ └── presentation │ │ │ │ ├── BaseApplication.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── UIController.kt │ │ │ │ ├── common │ │ │ │ ├── BaseNoteFragment.kt │ │ │ │ ├── BaseViewModel.kt │ │ │ │ ├── KeyboardExtensions.kt │ │ │ │ ├── NoteFragmentFactory.kt │ │ │ │ ├── NoteViewModelFactory.kt │ │ │ │ ├── TopSpacingItemDecoration.kt │ │ │ │ └── ViewExtensions.kt │ │ │ │ ├── notedetail │ │ │ │ ├── NoteDetailFragment.kt │ │ │ │ ├── NoteDetailViewModel.kt │ │ │ │ └── state │ │ │ │ │ ├── CollapsingToolbarState.kt │ │ │ │ │ ├── NoteDetailStateEvent.kt │ │ │ │ │ ├── NoteDetailViewState.kt │ │ │ │ │ ├── NoteInteractionManager.kt │ │ │ │ │ └── NoteInteractionState.kt │ │ │ │ ├── notelist │ │ │ │ ├── NoteItemTouchHelperCallback.kt │ │ │ │ ├── NoteListAdapter.kt │ │ │ │ ├── NoteListFragment.kt │ │ │ │ ├── NoteListViewModel.kt │ │ │ │ └── state │ │ │ │ │ ├── NoteListInteractionManager.kt │ │ │ │ │ ├── NoteListStateEvent.kt │ │ │ │ │ ├── NoteListToolbarState.kt │ │ │ │ │ └── NoteListViewState.kt │ │ │ │ └── splash │ │ │ │ ├── NoteNetworkSyncManager.kt │ │ │ │ ├── SplashFragment.kt │ │ │ │ └── SplashViewModel.kt │ │ │ └── util │ │ │ ├── AndroidTestUtils.kt │ │ │ ├── Constants.kt │ │ │ ├── Logger.kt │ │ │ └── TodoCallback.kt │ └── res │ │ ├── anim │ │ ├── fade_in.xml │ │ ├── fade_out.xml │ │ ├── slide_in_left.xml │ │ ├── slide_in_right.xml │ │ ├── slide_out_left.xml │ │ └── slide_out_right.xml │ │ ├── color │ │ └── bottom_nav_selector.xml │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_add_white_24dp.xml │ │ ├── ic_arrow_back_grey_24dp.xml │ │ ├── ic_close_grey_24dp.xml │ │ ├── ic_date_range_black_24dp.xml │ │ ├── ic_delete.xml │ │ ├── ic_done_grey_24dp.xml │ │ ├── ic_event_note_white_24dp.xml │ │ ├── ic_filter_list_grey_24dp.xml │ │ ├── ic_folder_white_24dp.xml │ │ ├── ic_launcher_background.xml │ │ ├── list_item_selector.xml │ │ ├── logo_250_250.png │ │ └── red_button_drawable.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── fragment_note_detail.xml │ │ ├── fragment_note_list.xml │ │ ├── fragment_splash.xml │ │ ├── layout_filter.xml │ │ ├── layout_multiselection_toolbar.xml │ │ ├── layout_note_detail_toolbar.xml │ │ ├── layout_note_list_item.xml │ │ └── layout_searchview_toolbar.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── navigation │ │ └── nav_app_graph.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimen.xml │ │ ├── strings.xml │ │ └── styles.xml │ ├── release │ └── java │ │ └── com │ │ └── codingwithmitch │ │ └── cleannotes │ │ └── util │ │ └── EspressoIdlingResource.kt │ └── test │ ├── java │ └── com │ │ └── codingwithmitch │ │ └── cleannotes │ │ ├── business │ │ ├── data │ │ │ ├── NoteDataFactory.kt │ │ │ ├── cache │ │ │ │ └── FakeNoteCacheDataSourceImpl.kt │ │ │ └── network │ │ │ │ └── FakeNoteNetworkDataSourceImpl.kt │ │ └── interactors │ │ │ ├── common │ │ │ └── DeleteNoteTest.kt │ │ │ ├── notedetail │ │ │ └── UpdateNoteTest.kt │ │ │ ├── notelist │ │ │ ├── DeleteMultipleNotesTest.kt │ │ │ ├── GetNumNotesTest.kt │ │ │ ├── InsertNewNoteTest.kt │ │ │ ├── RestoreDeletedNoteTest.kt │ │ │ └── SearchNotesTest.kt │ │ │ └── splash │ │ │ ├── SyncDeletedNotesTest.kt │ │ │ └── SyncNotesTest.kt │ │ └── di │ │ └── DependencyContainer.kt │ └── res │ └── note_list.json ├── build.gradle ├── buildSrc ├── build.gradle.kts ├── build │ ├── classes │ │ └── kotlin │ │ │ └── main │ │ │ ├── META-INF │ │ │ └── buildSrc.kotlin_module │ │ │ └── dependencies │ │ │ ├── AnnotationProcessing.class │ │ │ ├── Application.class │ │ │ ├── Build.class │ │ │ ├── Java.class │ │ │ ├── Repositories.class │ │ │ ├── Versions.class │ │ │ └── dependencies │ │ │ ├── AndroidTestDependencies.class │ │ │ ├── Dependencies.class │ │ │ ├── SupportDependencies.class │ │ │ └── TestDependencies.class │ ├── kotlin │ │ ├── buildSrcjar-classes.txt │ │ └── compileKotlin │ │ │ ├── build-history.bin │ │ │ ├── caches-jvm │ │ │ ├── inputs │ │ │ │ ├── source-to-output.tab │ │ │ │ └── source-to-output.tab_i │ │ │ ├── jvm │ │ │ │ └── kotlin │ │ │ │ │ ├── class-fq-name-to-source.tab │ │ │ │ │ ├── class-fq-name-to-source.tab_i │ │ │ │ │ ├── internal-name-to-source.tab │ │ │ │ │ ├── internal-name-to-source.tab_i │ │ │ │ │ ├── proto.tab │ │ │ │ │ ├── proto.tab_i │ │ │ │ │ └── source-to-classes.tab │ │ │ └── lookups │ │ │ │ ├── file-to-id.tab │ │ │ │ ├── file-to-id.tab_i │ │ │ │ ├── id-to-file.tab │ │ │ │ ├── lookups.tab │ │ │ │ └── lookups.tab_i │ │ │ └── last-build.bin │ ├── libs │ │ └── buildSrc.jar │ ├── pluginUnderTestMetadata │ │ └── plugin-under-test-metadata.properties │ ├── reports │ │ └── task-properties │ │ │ └── report.txt │ └── tmp │ │ └── jar │ │ └── MANIFEST.MF └── src │ └── main │ └── java │ ├── AnnotationProcessing.kt │ ├── Application.kt │ ├── Build.kt │ ├── Java.kt │ ├── Repositories.kt │ ├── Versions.kt │ └── dependencies │ ├── AndroidTestDependencies.kt │ ├── Dependencies.kt │ ├── SupportDependencies.kt │ └── TestDependencies.kt ├── firebase-debug.log ├── firebase.json ├── firestore-debug.log ├── firestore.indexes.json ├── firestore.rules ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle ├── tests ├── firestore-debug.log ├── run_tests.sh └── ui_and_unit_tests.sh └── ui-debug.log /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "clean-notes-11971" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Clean Notes -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Clean Architecture](https://codingwithmitch.com/courses/android-clean-architecture/) 2 | 3 | Android example with Clean Architecture by layer. 4 | 5 | **Watch the course here:** [Clean Architecture](https://codingwithmitch.com/courses/android-clean-architecture/) 6 | 7 | **NOTE** I got rid of dynamic feature modules because you cannot write tests currently. See bug: [google issue tracker](https://issuetracker.google.com/issues/145191501). 8 | 9 | In the future I will make another course on Dynamic Feature Modules. 10 | 11 | ![logo](https://codingwithmitch.s3.amazonaws.com/static/courses/21/clean_architecture_diagrams.png) 12 | 13 | # Running this app 14 | 15 | To run this app you will need to create a firebase project and hook it up with the project. I password protected the login of mine so you won't be able to get into the app. 16 | 17 | # Running the Tests 18 | 1. cd into /tests/ 19 | 2. type in terminal: `run_tests.sh` 20 | This will run all the unit tests and instrumentation tests. It will also start the firebase emulator to simulate firestore db. 21 | **The test results** are in `/app/build/reports/`. 22 | 23 | # Credits 24 | 1. https://proandroiddev.com/gradle-dependency-management-with-kotlin-94eed4df9a28 25 | 2. https://proandroiddev.com/intro-to-app-modularization-42411e4c421e 26 | 3. https://www.droidcon.com/media-detail?video=380845032 27 | 4. https://proandroiddev.com/kotlin-clean-architecture-1ad42fcd97fa 28 | 5. https://www.raywenderlich.com/3595916-clean-architecture-tutorial-for-android-getting-started 29 | 6. http://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html 30 | 7. https://developer.android.com/guide/navigation/navigation-dynamic 31 | 8. https://proandroiddev.com/navigation-with-dynamic-feature-modules-48ee7645488 32 | 9. https://medium.com/androiddevelopers/patterns-for-accessing-code-from-dynamic-feature-modules-7e5dca6f9123 33 | 10. https://hackernoon.com/android-components-architecture-in-a-modular-word-d0k32i6 34 | -------------------------------------------------------------------------------- /Task: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/Task -------------------------------------------------------------------------------- /There: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/There -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "907934723655", 4 | "firebase_url": "https://clean-notes-11971.firebaseio.com", 5 | "project_id": "clean-notes-11971", 6 | "storage_bucket": "clean-notes-11971.appspot.com" 7 | }, 8 | "client": [ 9 | { 10 | "client_info": { 11 | "mobilesdk_app_id": "1:907934723655:android:64933e0389125b1c834b62", 12 | "android_client_info": { 13 | "package_name": "com.codingwithmitch.cleannotes" 14 | } 15 | }, 16 | "oauth_client": [ 17 | { 18 | "client_id": "907934723655-qh5n32pf7j3b7assa9epvvisoturj5hf.apps.googleusercontent.com", 19 | "client_type": 3 20 | } 21 | ], 22 | "api_key": [ 23 | { 24 | "current_key": "AIzaSyAHu39nDajD4_6aXppfVQAClShHNl7DXIE" 25 | } 26 | ], 27 | "services": { 28 | "appinvite_service": { 29 | "other_platform_oauth_client": [ 30 | { 31 | "client_id": "907934723655-qh5n32pf7j3b7assa9epvvisoturj5hf.apps.googleusercontent.com", 32 | "client_type": 3 33 | } 34 | ] 35 | } 36 | } 37 | } 38 | ], 39 | "configuration_version": "1" 40 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/codingwithmitch/cleannotes/BaseTest.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import androidx.test.core.app.ApplicationProvider 6 | import androidx.test.espresso.Espresso.onView 7 | import androidx.test.espresso.IdlingRegistry 8 | import androidx.test.espresso.IdlingResource 9 | import androidx.test.espresso.assertion.ViewAssertions.doesNotExist 10 | import androidx.test.espresso.assertion.ViewAssertions.matches 11 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed 12 | import androidx.test.espresso.matcher.ViewMatchers.withId 13 | import com.codingwithmitch.cleannotes.framework.presentation.TestBaseApplication 14 | import com.codingwithmitch.cleannotes.util.ViewShownIdlingResource 15 | import kotlinx.coroutines.ExperimentalCoroutinesApi 16 | import kotlinx.coroutines.FlowPreview 17 | import org.hamcrest.Matcher 18 | 19 | 20 | @ExperimentalCoroutinesApi 21 | @FlowPreview 22 | abstract class BaseTest { 23 | 24 | val application: TestBaseApplication 25 | = ApplicationProvider.getApplicationContext() as TestBaseApplication 26 | 27 | abstract fun injectTest() 28 | 29 | // wait for a certain view to be shown. 30 | // ex: waiting for splash screen to transition to NoteListFragment 31 | fun waitViewShown(matcher: Matcher) { 32 | val idlingResource: IdlingResource = ViewShownIdlingResource(matcher, isDisplayed()) 33 | try { 34 | IdlingRegistry.getInstance().register(idlingResource) 35 | onView(withId(0)).check(doesNotExist()) 36 | } finally { 37 | IdlingRegistry.getInstance().unregister(idlingResource) 38 | } 39 | } 40 | } 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/codingwithmitch/cleannotes/InstrumentationTestSuite.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes 2 | 3 | import com.codingwithmitch.cleannotes.framework.datasource.cache.NoteDaoServiceTests 4 | import com.codingwithmitch.cleannotes.framework.datasource.network.NoteFirestoreServiceTests 5 | import com.codingwithmitch.cleannotes.framework.presentation.end_to_end.NotesFeatureTest 6 | import com.codingwithmitch.cleannotes.framework.presentation.notedetail.NoteDetailFragmentTests 7 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.NoteListFragmentTests 8 | import kotlinx.coroutines.ExperimentalCoroutinesApi 9 | import kotlinx.coroutines.FlowPreview 10 | import kotlinx.coroutines.InternalCoroutinesApi 11 | import org.junit.runner.RunWith 12 | import org.junit.runners.Suite 13 | 14 | 15 | @FlowPreview 16 | @ExperimentalCoroutinesApi 17 | @InternalCoroutinesApi 18 | @RunWith(Suite::class) 19 | @Suite.SuiteClasses( 20 | NoteDaoServiceTests::class, 21 | NoteFirestoreServiceTests::class, 22 | NoteDetailFragmentTests::class, 23 | NoteListFragmentTests::class, 24 | NotesFeatureTest::class 25 | ) 26 | class InstrumentationTestSuite 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/codingwithmitch/cleannotes/di/TestAppComponent.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.di 2 | 3 | import com.codingwithmitch.cleannotes.framework.datasource.cache.NoteDaoServiceTests 4 | import com.codingwithmitch.cleannotes.framework.datasource.network.NoteFirestoreServiceTests 5 | import com.codingwithmitch.cleannotes.framework.presentation.TestBaseApplication 6 | import com.codingwithmitch.cleannotes.framework.presentation.end_to_end.NotesFeatureTest 7 | import com.codingwithmitch.cleannotes.framework.presentation.notedetail.NoteDetailFragmentTests 8 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.NoteListFragmentTests 9 | import com.codingwithmitch.cleannotes.notes.di.NoteViewModelModule 10 | import dagger.BindsInstance 11 | import dagger.Component 12 | import kotlinx.coroutines.ExperimentalCoroutinesApi 13 | import kotlinx.coroutines.FlowPreview 14 | import javax.inject.Singleton 15 | 16 | @ExperimentalCoroutinesApi 17 | @FlowPreview 18 | @Singleton 19 | @Component( 20 | modules = [ 21 | TestModule::class, 22 | AppModule::class, 23 | TestNoteFragmentFactoryModule::class, 24 | NoteViewModelModule::class 25 | ] 26 | ) 27 | interface TestAppComponent: AppComponent { 28 | 29 | @Component.Factory 30 | interface Factory{ 31 | 32 | fun create(@BindsInstance app: TestBaseApplication): TestAppComponent 33 | } 34 | 35 | fun inject(noteDaoServiceTests: NoteDaoServiceTests) 36 | 37 | fun inject(firestoreServiceTests: NoteFirestoreServiceTests) 38 | 39 | fun inject(noteListFragmentTests: NoteListFragmentTests) 40 | 41 | fun inject(noteDetailFragmentTests: NoteDetailFragmentTests) 42 | 43 | fun inject(notesFeatureTest: NotesFeatureTest) 44 | } 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/codingwithmitch/cleannotes/di/TestModule.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.di 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import androidx.room.Room 6 | import com.codingwithmitch.cleannotes.business.domain.model.NoteFactory 7 | import com.codingwithmitch.cleannotes.framework.datasource.cache.database.NoteDatabase 8 | import com.codingwithmitch.cleannotes.framework.datasource.data.NoteDataFactory 9 | import com.codingwithmitch.cleannotes.framework.datasource.preferences.PreferenceKeys 10 | import com.codingwithmitch.cleannotes.framework.presentation.TestBaseApplication 11 | import com.codingwithmitch.cleannotes.util.AndroidTestUtils 12 | import com.google.firebase.firestore.FirebaseFirestore 13 | import com.google.firebase.firestore.FirebaseFirestoreSettings 14 | import dagger.Module 15 | import dagger.Provides 16 | import kotlinx.coroutines.ExperimentalCoroutinesApi 17 | import kotlinx.coroutines.FlowPreview 18 | import javax.inject.Singleton 19 | 20 | @ExperimentalCoroutinesApi 21 | @FlowPreview 22 | @Module 23 | object TestModule { 24 | 25 | @JvmStatic 26 | @Singleton 27 | @Provides 28 | fun provideAndroidTestUtils(): AndroidTestUtils { 29 | return AndroidTestUtils(true) 30 | } 31 | 32 | @JvmStatic 33 | @Singleton 34 | @Provides 35 | fun provideSharedPreferences( 36 | application: TestBaseApplication 37 | ): SharedPreferences { 38 | return application 39 | .getSharedPreferences( 40 | PreferenceKeys.NOTE_PREFERENCES, 41 | Context.MODE_PRIVATE 42 | ) 43 | } 44 | 45 | @JvmStatic 46 | @Singleton 47 | @Provides 48 | fun provideNoteDb(app: TestBaseApplication): NoteDatabase { 49 | return Room 50 | .inMemoryDatabaseBuilder(app, NoteDatabase::class.java) 51 | .fallbackToDestructiveMigration() 52 | .build() 53 | } 54 | 55 | @JvmStatic 56 | @Singleton 57 | @Provides 58 | fun provideFirestoreSettings(): FirebaseFirestoreSettings { 59 | return FirebaseFirestoreSettings.Builder() 60 | .setHost("10.0.2.2:8080") 61 | .setSslEnabled(false) 62 | .setPersistenceEnabled(false) 63 | .build() 64 | } 65 | 66 | @JvmStatic 67 | @Singleton 68 | @Provides 69 | fun provideFirebaseFirestore( 70 | firestoreSettings: FirebaseFirestoreSettings 71 | ): FirebaseFirestore { 72 | val firestore = FirebaseFirestore.getInstance() 73 | firestore.firestoreSettings = firestoreSettings 74 | return firestore 75 | } 76 | 77 | @JvmStatic 78 | @Singleton 79 | @Provides 80 | fun provideNoteDataFactory( 81 | application: TestBaseApplication, 82 | noteFactory: NoteFactory 83 | ): NoteDataFactory{ 84 | return NoteDataFactory(application, noteFactory) 85 | } 86 | } 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/codingwithmitch/cleannotes/di/TestNoteFragmentFactoryModule.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.di 2 | 3 | import androidx.fragment.app.FragmentFactory 4 | import androidx.lifecycle.ViewModelProvider 5 | import com.codingwithmitch.cleannotes.business.domain.util.DateUtil 6 | import com.codingwithmitch.cleannotes.framework.presentation.TestNoteFragmentFactory 7 | import dagger.Module 8 | import dagger.Provides 9 | import kotlinx.coroutines.ExperimentalCoroutinesApi 10 | import kotlinx.coroutines.FlowPreview 11 | import javax.inject.Singleton 12 | 13 | @FlowPreview 14 | @ExperimentalCoroutinesApi 15 | @Module 16 | object TestNoteFragmentFactoryModule { 17 | 18 | @Singleton 19 | @JvmStatic 20 | @Provides 21 | fun provideNoteFragmentFactory( 22 | viewModelFactory: ViewModelProvider.Factory, 23 | dateUtil: DateUtil 24 | ): FragmentFactory { 25 | return TestNoteFragmentFactory(viewModelFactory, dateUtil) 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/codingwithmitch/cleannotes/framework/MockTestRunner.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import androidx.test.runner.AndroidJUnitRunner 6 | import com.codingwithmitch.cleannotes.framework.presentation.TestBaseApplication 7 | import kotlinx.coroutines.ExperimentalCoroutinesApi 8 | import kotlinx.coroutines.FlowPreview 9 | 10 | @FlowPreview 11 | @ExperimentalCoroutinesApi 12 | class MockTestRunner: AndroidJUnitRunner(){ 13 | 14 | override fun newApplication( 15 | cl: ClassLoader?, 16 | className: String?, 17 | context: Context? 18 | ) : Application { 19 | return super.newApplication(cl, TestBaseApplication::class.java.name, context) 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/codingwithmitch/cleannotes/framework/datasource/data/NoteDataFactory.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.datasource.data 2 | 3 | import android.app.Application 4 | import android.content.res.AssetManager 5 | import com.codingwithmitch.cleannotes.business.domain.model.Note 6 | import com.codingwithmitch.cleannotes.business.domain.model.NoteFactory 7 | import com.google.common.reflect.TypeToken 8 | import com.google.gson.Gson 9 | import java.io.IOException 10 | import java.io.InputStream 11 | import java.util.* 12 | import javax.inject.Inject 13 | import javax.inject.Singleton 14 | import kotlin.collections.ArrayList 15 | 16 | @Singleton 17 | class NoteDataFactory 18 | @Inject 19 | constructor( 20 | private val application: Application, 21 | private val noteFactory: NoteFactory 22 | ){ 23 | 24 | fun produceListOfNotes(): List{ 25 | val notes: List = Gson() 26 | .fromJson( 27 | getNotesFromFile("note_list.json"), 28 | object: TypeToken>() {}.type 29 | ) 30 | return notes 31 | } 32 | 33 | fun produceEmptyListOfNotes(): List{ 34 | return ArrayList() 35 | } 36 | 37 | fun getNotesFromFile(fileName: String): String? { 38 | return readJSONFromAsset(fileName) 39 | } 40 | 41 | private fun readJSONFromAsset(fileName: String): String? { 42 | var json: String? = null 43 | json = try { 44 | val inputStream: InputStream = (application.assets as AssetManager).open(fileName) 45 | inputStream.bufferedReader().use{it.readText()} 46 | } catch (ex: IOException) { 47 | ex.printStackTrace() 48 | return null 49 | } 50 | return json 51 | } 52 | 53 | fun createSingleNote( 54 | id: String? = null, 55 | title: String, 56 | body: String? = null 57 | ) = noteFactory.createSingleNote(id, title, body) 58 | 59 | fun createNoteList(numNotes: Int) = noteFactory.createNoteList(numNotes) 60 | } 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/codingwithmitch/cleannotes/framework/presentation/TestBaseApplication.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.presentation 2 | 3 | import com.codingwithmitch.cleannotes.di.DaggerTestAppComponent 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.FlowPreview 6 | 7 | @FlowPreview 8 | @ExperimentalCoroutinesApi 9 | class TestBaseApplication : BaseApplication(){ 10 | 11 | override fun initAppComponent() { 12 | appComponent = DaggerTestAppComponent 13 | .factory() 14 | .create(this) 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/codingwithmitch/cleannotes/framework/presentation/TestNoteFragmentFactory.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.presentation 2 | 3 | import androidx.fragment.app.FragmentFactory 4 | import androidx.lifecycle.ViewModelProvider 5 | import com.codingwithmitch.cleannotes.business.domain.util.DateUtil 6 | import com.codingwithmitch.cleannotes.framework.presentation.notedetail.NoteDetailFragment 7 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.NoteListFragment 8 | import com.codingwithmitch.cleannotes.framework.presentation.splash.SplashFragment 9 | import kotlinx.coroutines.ExperimentalCoroutinesApi 10 | import kotlinx.coroutines.FlowPreview 11 | import javax.inject.Inject 12 | import javax.inject.Singleton 13 | 14 | @ExperimentalCoroutinesApi 15 | @FlowPreview 16 | @Singleton 17 | class TestNoteFragmentFactory 18 | @Inject 19 | constructor( 20 | private val viewModelFactory: ViewModelProvider.Factory, 21 | private val dateUtil: DateUtil 22 | ): FragmentFactory(){ 23 | 24 | lateinit var uiController: UIController 25 | 26 | override fun instantiate(classLoader: ClassLoader, className: String) = 27 | 28 | when(className){ 29 | 30 | NoteListFragment::class.java.name -> { 31 | val fragment = NoteListFragment(viewModelFactory, dateUtil) 32 | if(::uiController.isInitialized){ 33 | fragment.setUIController(uiController) 34 | } 35 | fragment 36 | } 37 | 38 | NoteDetailFragment::class.java.name -> { 39 | val fragment = NoteDetailFragment(viewModelFactory) 40 | if(::uiController.isInitialized){ 41 | fragment.setUIController(uiController) 42 | } 43 | fragment 44 | } 45 | 46 | SplashFragment::class.java.name -> { 47 | val fragment = SplashFragment(viewModelFactory) 48 | if(::uiController.isInitialized){ 49 | fragment.setUIController(uiController) 50 | } 51 | fragment 52 | } 53 | 54 | else -> { 55 | super.instantiate(classLoader, className) 56 | } 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/codingwithmitch/cleannotes/util/EspressoIdlingResourceRule.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.util 2 | 3 | import androidx.test.espresso.IdlingRegistry 4 | import org.junit.rules.TestWatcher 5 | import org.junit.runner.Description 6 | 7 | class EspressoIdlingResourceRule : TestWatcher(){ 8 | 9 | private val CLASS_NAME = "EspressoIdlingResourceRule" 10 | 11 | private val idlingResource = EspressoIdlingResource.countingIdlingResource 12 | 13 | override fun finished(description: Description?) { 14 | printLogD(CLASS_NAME, "FINISHED") 15 | IdlingRegistry.getInstance().unregister(idlingResource) 16 | super.finished(description) 17 | } 18 | 19 | override fun starting(description: Description?) { 20 | printLogD(CLASS_NAME, "STARTING") 21 | IdlingRegistry.getInstance().register(idlingResource) 22 | super.starting(description) 23 | } 24 | 25 | 26 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/codingwithmitch/cleannotes/util/ViewShownIdlingResource.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.util 2 | 3 | import android.view.View 4 | import androidx.test.espresso.Espresso 5 | import androidx.test.espresso.IdlingResource 6 | import androidx.test.espresso.IdlingResource.ResourceCallback 7 | import androidx.test.espresso.ViewFinder 8 | import org.hamcrest.Matcher 9 | 10 | /* 11 | Author: https://stackoverflow.com/questions/50628219/is-it-possible-to-use-espressos-idlingresource-to-wait-until-a-certain-view-app 12 | */ 13 | class ViewShownIdlingResource( 14 | private val viewMatcher: Matcher, 15 | private val idlerMatcher: Matcher 16 | ) : IdlingResource { 17 | 18 | private var resourceCallback: ResourceCallback? = null 19 | 20 | override fun isIdleNow(): Boolean { 21 | val view: View? = getView(viewMatcher) 22 | val idle = idlerMatcher.matches(view) 23 | if (idle && resourceCallback != null) { 24 | resourceCallback!!.onTransitionToIdle() 25 | } 26 | return idle 27 | } 28 | 29 | override fun registerIdleTransitionCallback(resourceCallback: ResourceCallback) { 30 | this.resourceCallback = resourceCallback 31 | } 32 | 33 | override fun getName(): String { 34 | return this.toString() + viewMatcher.toString() 35 | } 36 | 37 | 38 | companion object { 39 | 40 | private fun getView(viewMatcher: Matcher): View? { 41 | return try { 42 | val viewInteraction = Espresso.onView(viewMatcher) 43 | val finderField = 44 | viewInteraction.javaClass.getDeclaredField("viewFinder") 45 | finderField.isAccessible = true 46 | val finder = finderField[viewInteraction] as ViewFinder 47 | finder.view 48 | } catch (e: Exception) { 49 | null 50 | } 51 | } 52 | 53 | } 54 | 55 | 56 | } -------------------------------------------------------------------------------- /app/src/debug/assets/note_list.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "2474abea-7584-486b-9f88-87a21870b0ec", 4 | "title": "Vancouver PNE 2019", 5 | "body": "Here is Jess and I at the Vancouver PNE. We ate a lot of food.", 6 | "updated_at": "2019-04-14 08:41:22 AM", 7 | "created_at": "2019-04-14 07:05:11 AM" 8 | }, 9 | { 10 | "id": "2474fbaa-7884-4h6b-9b8z-87a21670b0ec", 11 | "title": "Ready for a Walk", 12 | "body": "Here I am at the park with my dogs Kiba and Maizy. Maizy is the smaller one and Kiba is the larger one.", 13 | "updated_at": "2019-04-17 11:05:24 PM", 14 | "created_at": "2019-04-15 04:44:57 AM" 15 | }, 16 | { 17 | "id": "2474mbaa-7884-4htb-9baz-87a216a0b0ec", 18 | "title": "Maizy Sleeping", 19 | "body": "I took this picture while Maizy was sleeping on the couch. She's very cute.", 20 | "updated_at": "2019-02-01 01:55:53 AM", 21 | "created_at": "2019-01-24 12:19:35 PM" 22 | }, 23 | { 24 | "id": "2474fpaa-k884-4u6b-9biz-87am1670b0ec", 25 | "title": "My Brother Blake", 26 | "body": "This is a picture of my brother Blake and I. We were taking some pictures for his website.", 27 | "updated_at": "2019-12-14 03:05:16 PM", 28 | "created_at": "2019-12-13 07:05:17 AM" 29 | }, 30 | { 31 | "id": "2474abaa-788a-4a6b-948z-87a2167hb0ec", 32 | "title": "Lounging Dogs", 33 | "body": "Kiba and Maizy are laying in the sun relaxing.", 34 | "updated_at": "2019-11-14 06:12:44 AM", 35 | "created_at": "2019-10-14 02:47:13 PM" 36 | }, 37 | { 38 | "id": "24742baa-78j4-4z6b-9b8l-87a11670b0ec", 39 | "title": "Mountains in Washington", 40 | "body": "This is an image I found somewhere on the internert. I love pictures like this. I believe it's in Washington, U.S.A.", 41 | "updated_at": "2019-05-19 11:34:16 PM", 42 | "created_at": "2019-04-25 05:16:36 AM" 43 | }, 44 | { 45 | "id": "2g74fbaa-78h4-4hab-9b85-87l21670b0ec", 46 | "title": "France Mountain Range", 47 | "body": "Another beautiful picture of nature. You can find more pictures like this one on Reddit.com, in the subreddit: '/r/earthporn'.", 48 | "updated_at": "2019-10-01 12:22:46 AM", 49 | "created_at": "2019-09-19 09:36:57 PM" 50 | }, 51 | { 52 | "id": "2477fbaa-7b84-4hjb-9bkl-87a2a670b0ec", 53 | "title": "Aldergrove Park", 54 | "body": "I walk Kiba and Maizy pretty much every day. Usually we go to a park in Aldergrove. It takes about 1 hour, 15 minutes to walk around the entire park.", 55 | "updated_at": "2019-06-12 12:58:55 AM", 56 | "created_at": "2019-03-19 08:49:41 PM" 57 | }, 58 | { 59 | "id": "2477fbbb-7b84-g5jb-9bkl-8741a670b0ec", 60 | "title": "My Computer", 61 | "body": "I sit on my computer all day.", 62 | "updated_at": "2019-03-11 12:58:55 AM", 63 | "created_at": "2019-01-15 11:49:41 PM" 64 | }, 65 | { 66 | "id": "247aaabb-7564-g5jb-9bkl-8741ah70b0ec", 67 | "title": "Courses", 68 | "body": "I make Android dev courses for a living.", 69 | "updated_at": "2019-05-04 09:44:55 AM", 70 | "created_at": "2019-01-15 10:11:41 PM" 71 | } 72 | ] -------------------------------------------------------------------------------- /app/src/debug/java/com/codingwithmitch/cleannotes/util/EspressoIdlingResource.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.util 2 | 3 | import androidx.test.espresso.idling.CountingIdlingResource 4 | 5 | 6 | object EspressoIdlingResource { 7 | 8 | private val CLASS_NAME = "EspressoIdlingResource" 9 | 10 | private const val RESOURCE = "GLOBAL" 11 | 12 | @JvmField val countingIdlingResource = CountingIdlingResource(RESOURCE) 13 | 14 | fun increment() { 15 | printLogD(CLASS_NAME, "INCREMENTING.") 16 | countingIdlingResource.increment() 17 | } 18 | 19 | fun decrement() { 20 | if (!countingIdlingResource.isIdleNow) { 21 | printLogD(CLASS_NAME, "DECREMENTING.") 22 | countingIdlingResource.decrement() 23 | } 24 | } 25 | 26 | fun clear() { 27 | if (!countingIdlingResource.isIdleNow) { 28 | decrement() 29 | clear() 30 | } 31 | } 32 | } 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/data/cache/CacheConstants.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.data.cache 2 | 3 | object CacheConstants { 4 | 5 | const val CACHE_TIMEOUT = 2000L 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/data/cache/CacheErrors.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.data.cache 2 | 3 | object CacheErrors { 4 | 5 | const val CACHE_ERROR_UNKNOWN = "Unknown cache error" 6 | const val CACHE_ERROR = "Cache error" 7 | const val CACHE_ERROR_TIMEOUT = "Cache timeout" 8 | const val CACHE_DATA_NULL = "Cache data is null" 9 | 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/data/cache/CacheResponseHandler.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.data.cache 2 | 3 | import com.codingwithmitch.cleannotes.business.data.cache.CacheErrors.CACHE_DATA_NULL 4 | import com.codingwithmitch.cleannotes.business.domain.state.* 5 | 6 | 7 | abstract class CacheResponseHandler ( 8 | private val response: CacheResult, 9 | private val stateEvent: StateEvent? 10 | ){ 11 | suspend fun getResult(): DataState? { 12 | 13 | return when(response){ 14 | 15 | is CacheResult.GenericError -> { 16 | DataState.error( 17 | response = Response( 18 | message = "${stateEvent?.errorInfo()}\n\nReason: ${response.errorMessage}", 19 | uiComponentType = UIComponentType.Dialog(), 20 | messageType = MessageType.Error() 21 | ), 22 | stateEvent = stateEvent 23 | ) 24 | } 25 | 26 | is CacheResult.Success -> { 27 | if(response.value == null){ 28 | DataState.error( 29 | response = Response( 30 | message = "${stateEvent?.errorInfo()}\n\nReason: ${CACHE_DATA_NULL}.", 31 | uiComponentType = UIComponentType.Dialog(), 32 | messageType = MessageType.Error() 33 | ), 34 | stateEvent = stateEvent 35 | ) 36 | } 37 | else{ 38 | handleSuccess(resultObj = response.value) 39 | } 40 | } 41 | 42 | } 43 | } 44 | 45 | abstract suspend fun handleSuccess(resultObj: Data): DataState? 46 | 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/data/cache/CacheResult.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.data.cache 2 | 3 | sealed class CacheResult { 4 | 5 | data class Success(val value: T): CacheResult() 6 | 7 | data class GenericError( 8 | val errorMessage: String? = null 9 | ): CacheResult() 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/data/cache/abstraction/NoteCacheDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.data.cache.abstraction 2 | 3 | import com.codingwithmitch.cleannotes.business.domain.model.Note 4 | 5 | interface NoteCacheDataSource{ 6 | 7 | suspend fun insertNote(note: Note): Long 8 | 9 | suspend fun deleteNote(primaryKey: String): Int 10 | 11 | suspend fun deleteNotes(notes: List): Int 12 | 13 | suspend fun updateNote( 14 | primaryKey: String, 15 | newTitle: String, 16 | newBody: String?, 17 | timestamp: String? 18 | ): Int 19 | 20 | suspend fun searchNotes( 21 | query: String, 22 | filterAndOrder: String, 23 | page: Int 24 | ): List 25 | 26 | suspend fun getAllNotes(): List 27 | 28 | suspend fun searchNoteById(id: String): Note? 29 | 30 | suspend fun getNumNotes(): Int 31 | 32 | suspend fun insertNotes(notes: List): LongArray 33 | } 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/data/cache/implementation/NoteCacheDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.data.cache.implementation 2 | 3 | import com.codingwithmitch.cleannotes.business.data.cache.abstraction.NoteCacheDataSource 4 | import com.codingwithmitch.cleannotes.business.domain.model.Note 5 | import com.codingwithmitch.cleannotes.framework.datasource.cache.abstraction.NoteDaoService 6 | import javax.inject.Inject 7 | import javax.inject.Singleton 8 | 9 | @Singleton 10 | class NoteCacheDataSourceImpl 11 | @Inject 12 | constructor( 13 | private val noteDaoService: NoteDaoService 14 | ): NoteCacheDataSource { 15 | 16 | override suspend fun insertNote(note: Note): Long { 17 | return noteDaoService.insertNote(note) 18 | } 19 | 20 | override suspend fun deleteNote(primaryKey: String): Int { 21 | return noteDaoService.deleteNote(primaryKey) 22 | } 23 | 24 | override suspend fun deleteNotes(notes: List): Int { 25 | return noteDaoService.deleteNotes(notes) 26 | } 27 | 28 | override suspend fun updateNote( 29 | primaryKey: String, 30 | newTitle: String, 31 | newBody: String?, 32 | timestamp: String? 33 | ): Int { 34 | return noteDaoService.updateNote( 35 | primaryKey, 36 | newTitle, 37 | newBody, 38 | timestamp 39 | ) 40 | } 41 | 42 | override suspend fun searchNotes( 43 | query: String, 44 | filterAndOrder: String, 45 | page: Int 46 | ): List { 47 | return noteDaoService.returnOrderedQuery( 48 | query, filterAndOrder, page 49 | ) 50 | } 51 | 52 | override suspend fun getAllNotes(): List { 53 | return noteDaoService.getAllNotes() 54 | } 55 | 56 | override suspend fun searchNoteById(id: String): Note? { 57 | return noteDaoService.searchNoteById(id) 58 | } 59 | 60 | override suspend fun getNumNotes(): Int { 61 | return noteDaoService.getNumNotes() 62 | } 63 | 64 | override suspend fun insertNotes(notes: List): LongArray{ 65 | return noteDaoService.insertNotes(notes) 66 | } 67 | } 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/data/network/ApiResponseHandler.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.data.network 2 | 3 | import com.codingwithmitch.cleannotes.business.data.network.NetworkErrors.NETWORK_DATA_NULL 4 | import com.codingwithmitch.cleannotes.business.data.network.NetworkErrors.NETWORK_ERROR 5 | import com.codingwithmitch.cleannotes.business.domain.state.* 6 | 7 | 8 | abstract class ApiResponseHandler ( 9 | private val response: ApiResult, 10 | private val stateEvent: StateEvent? 11 | ){ 12 | 13 | suspend fun getResult(): DataState? { 14 | 15 | return when(response){ 16 | 17 | is ApiResult.GenericError -> { 18 | DataState.error( 19 | response = Response( 20 | message = "${stateEvent?.errorInfo()}\n\nReason: ${response.errorMessage.toString()}", 21 | uiComponentType = UIComponentType.Dialog(), 22 | messageType = MessageType.Error() 23 | ), 24 | stateEvent = stateEvent 25 | ) 26 | } 27 | 28 | is ApiResult.NetworkError -> { 29 | DataState.error( 30 | response = Response( 31 | message = "${stateEvent?.errorInfo()}\n\nReason: ${NETWORK_ERROR}", 32 | uiComponentType = UIComponentType.Dialog(), 33 | messageType = MessageType.Error() 34 | ), 35 | stateEvent = stateEvent 36 | ) 37 | } 38 | 39 | is ApiResult.Success -> { 40 | if(response.value == null){ 41 | DataState.error( 42 | response = Response( 43 | message = "${stateEvent?.errorInfo()}\n\nReason: ${NETWORK_DATA_NULL}.", 44 | uiComponentType = UIComponentType.Dialog(), 45 | messageType = MessageType.Error() 46 | ), 47 | stateEvent = stateEvent 48 | ) 49 | } 50 | else{ 51 | handleSuccess(resultObj = response.value) 52 | } 53 | } 54 | 55 | } 56 | } 57 | 58 | abstract suspend fun handleSuccess(resultObj: Data): DataState? 59 | 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/data/network/ApiResult.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.data.network 2 | 3 | sealed class ApiResult { 4 | 5 | data class Success(val value: T): ApiResult() 6 | 7 | data class GenericError( 8 | val code: Int? = null, 9 | val errorMessage: String? = null 10 | ): ApiResult() 11 | 12 | object NetworkError: ApiResult() 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/data/network/NetworkConstants.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.data.network 2 | 3 | object NetworkConstants { 4 | 5 | const val NETWORK_TIMEOUT = 6000L 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/data/network/NetworkErrors.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.data.network 2 | 3 | object NetworkErrors { 4 | 5 | const val UNABLE_TO_RESOLVE_HOST = "Unable to resolve host" 6 | const val UNABLE_TODO_OPERATION_WO_INTERNET = "Can't do that operation without an internet connection" 7 | const val ERROR_CHECK_NETWORK_CONNECTION = "Check network connection." 8 | const val NETWORK_ERROR_UNKNOWN = "Unknown network error" 9 | const val NETWORK_ERROR = "Network error" 10 | const val NETWORK_ERROR_TIMEOUT = "Network timeout" 11 | const val NETWORK_DATA_NULL = "Network data is null" 12 | 13 | 14 | fun isNetworkError(msg: String): Boolean{ 15 | when{ 16 | msg.contains(UNABLE_TO_RESOLVE_HOST) -> return true 17 | else-> return false 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/data/network/abstraction/NoteNetworkDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.data.network.abstraction 2 | 3 | import com.codingwithmitch.cleannotes.business.domain.model.Note 4 | 5 | 6 | interface NoteNetworkDataSource{ 7 | 8 | suspend fun insertOrUpdateNote(note: Note) 9 | 10 | suspend fun deleteNote(primaryKey: String) 11 | 12 | suspend fun insertDeletedNote(note: Note) 13 | 14 | suspend fun insertDeletedNotes(notes: List) 15 | 16 | suspend fun deleteDeletedNote(note: Note) 17 | 18 | suspend fun getDeletedNotes(): List 19 | 20 | suspend fun deleteAllNotes() 21 | 22 | suspend fun searchNote(note: Note): Note? 23 | 24 | suspend fun getAllNotes(): List 25 | 26 | suspend fun insertOrUpdateNotes(notes: List) 27 | 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/data/network/implementation/NoteNetworkDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.data.network.implementation 2 | 3 | import com.codingwithmitch.cleannotes.business.data.network.abstraction.NoteNetworkDataSource 4 | import com.codingwithmitch.cleannotes.business.domain.model.Note 5 | import com.codingwithmitch.cleannotes.framework.datasource.network.abstraction.NoteFirestoreService 6 | import javax.inject.Inject 7 | import javax.inject.Singleton 8 | 9 | 10 | @Singleton 11 | class NoteNetworkDataSourceImpl 12 | @Inject 13 | constructor( 14 | private val firestoreService: NoteFirestoreService 15 | ): NoteNetworkDataSource { 16 | 17 | override suspend fun insertOrUpdateNote(note: Note) { 18 | return firestoreService.insertOrUpdateNote(note) 19 | } 20 | 21 | override suspend fun deleteNote(primaryKey: String) { 22 | return firestoreService.deleteNote(primaryKey) 23 | } 24 | 25 | override suspend fun insertDeletedNote(note: Note) { 26 | return firestoreService.insertDeletedNote(note) 27 | } 28 | 29 | override suspend fun insertDeletedNotes(notes: List) { 30 | return firestoreService.insertDeletedNotes(notes) 31 | } 32 | 33 | override suspend fun deleteDeletedNote(note: Note) { 34 | return firestoreService.deleteDeletedNote(note) 35 | } 36 | 37 | override suspend fun getDeletedNotes(): List { 38 | return firestoreService.getDeletedNotes() 39 | } 40 | 41 | override suspend fun deleteAllNotes() { 42 | firestoreService.deleteAllNotes() 43 | } 44 | 45 | override suspend fun searchNote(note: Note): Note? { 46 | return firestoreService.searchNote(note) 47 | } 48 | 49 | override suspend fun getAllNotes(): List { 50 | return firestoreService.getAllNotes() 51 | } 52 | 53 | override suspend fun insertOrUpdateNotes(notes: List) { 54 | return firestoreService.insertOrUpdateNotes(notes) 55 | } 56 | 57 | 58 | } 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/data/util/GenericErrors.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.data.util 2 | 3 | object GenericErrors { 4 | 5 | const val ERROR_UNKNOWN = "Unknown error" 6 | const val INVALID_STATE_EVENT = "Invalid state event" 7 | 8 | } 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/domain/model/Note.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.domain.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.android.parcel.Parcelize 5 | 6 | @Parcelize 7 | data class Note( 8 | val id: String, 9 | val title: String, 10 | val body: String, 11 | val updated_at: String, 12 | val created_at: String 13 | ) : Parcelable{ 14 | 15 | override fun equals(other: Any?): Boolean { 16 | if (this === other) return true 17 | if (javaClass != other?.javaClass) return false 18 | 19 | other as Note 20 | 21 | if (id != other.id) return false 22 | if (title != other.title) return false 23 | if (body != other.body) return false 24 | if (created_at != other.created_at) return false 25 | 26 | return true 27 | } 28 | 29 | override fun hashCode(): Int { 30 | var result = id.hashCode() 31 | result = 31 * result + title.hashCode() 32 | result = 31 * result + body.hashCode() 33 | result = 31 * result + created_at.hashCode() 34 | return result 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/domain/model/NoteFactory.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.domain.model 2 | 3 | import com.codingwithmitch.cleannotes.business.domain.util.DateUtil 4 | import java.util.* 5 | import javax.inject.Inject 6 | import javax.inject.Singleton 7 | import kotlin.collections.ArrayList 8 | 9 | @Singleton 10 | class NoteFactory 11 | @Inject 12 | constructor( 13 | private val dateUtil: DateUtil 14 | ) { 15 | 16 | fun createSingleNote( 17 | id: String? = null, 18 | title: String, 19 | body: String? = null 20 | ): Note { 21 | return Note( 22 | id = id ?: UUID.randomUUID().toString(), 23 | title = title, 24 | body = body ?: "", 25 | created_at = dateUtil.getCurrentTimestamp(), 26 | updated_at = dateUtil.getCurrentTimestamp() 27 | ) 28 | } 29 | 30 | fun createNoteList(numNotes: Int): List { 31 | val list: ArrayList = ArrayList() 32 | for(i in 0 until numNotes){ // exclusive on upper bound 33 | list.add( 34 | createSingleNote( 35 | id = UUID.randomUUID().toString(), 36 | title = UUID.randomUUID().toString(), 37 | body = UUID.randomUUID().toString() 38 | ) 39 | ) 40 | } 41 | return list 42 | } 43 | 44 | 45 | } 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/domain/state/DataState.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.domain.state 2 | 3 | 4 | data class DataState( 5 | var stateMessage: StateMessage? = null, 6 | var data: T? = null, 7 | var stateEvent: StateEvent? = null 8 | ) { 9 | 10 | companion object { 11 | 12 | fun error( 13 | response: Response, 14 | stateEvent: StateEvent? 15 | ): DataState { 16 | return DataState( 17 | stateMessage = StateMessage( 18 | response 19 | ), 20 | data = null, 21 | stateEvent = stateEvent 22 | ) 23 | } 24 | 25 | fun data( 26 | response: Response?, 27 | data: T? = null, 28 | stateEvent: StateEvent? 29 | ): DataState { 30 | return DataState( 31 | stateMessage = response?.let { 32 | StateMessage( 33 | it 34 | ) 35 | }, 36 | data = data, 37 | stateEvent = stateEvent 38 | ) 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/domain/state/MessageStack.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.domain.state 2 | 3 | 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import com.codingwithmitch.cleannotes.util.printLogD 7 | import kotlinx.android.parcel.IgnoredOnParcel 8 | import java.lang.IndexOutOfBoundsException 9 | 10 | const val MESSAGE_STACK_BUNDLE_KEY = "com.codingwithmitch.openapi.util.MessageStack" 11 | 12 | class MessageStack: ArrayList() { 13 | 14 | @IgnoredOnParcel 15 | private val _stateMessage: MutableLiveData = MutableLiveData() 16 | 17 | @IgnoredOnParcel 18 | val stateMessage: LiveData 19 | get() = _stateMessage 20 | 21 | fun isStackEmpty(): Boolean{ 22 | return size == 0 23 | } 24 | 25 | override fun addAll(elements: Collection): Boolean { 26 | for(element in elements){ 27 | add(element) 28 | } 29 | return true // always return true. We don't care about result bool. 30 | } 31 | 32 | override fun add(element: StateMessage): Boolean { 33 | if(this.contains(element)){ // prevent duplicate errors added to stack 34 | return false 35 | } 36 | val transaction = super.add(element) 37 | if(this.size == 1){ 38 | setStateMessage(stateMessage = element) 39 | } 40 | return transaction 41 | } 42 | 43 | override fun removeAt(index: Int): StateMessage { 44 | try{ 45 | val transaction = super.removeAt(index) 46 | if(this.size > 0){ 47 | setStateMessage(stateMessage = this[0]) 48 | } 49 | else{ 50 | printLogD("MessageStack", "stack is empty: ") 51 | setStateMessage(null) 52 | } 53 | return transaction 54 | }catch (e: IndexOutOfBoundsException){ 55 | setStateMessage(null) 56 | e.printStackTrace() 57 | } 58 | return StateMessage( // this does nothing 59 | Response( 60 | message = "does nothing", 61 | uiComponentType = UIComponentType.None(), 62 | messageType = MessageType.None() 63 | ) 64 | ) 65 | } 66 | 67 | private fun setStateMessage(stateMessage: StateMessage?){ 68 | _stateMessage.value = stateMessage 69 | } 70 | } 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/domain/state/StateEvent.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.domain.state 2 | 3 | interface StateEvent { 4 | 5 | fun errorInfo(): String 6 | 7 | fun eventName(): String 8 | 9 | fun shouldDisplayProgressBar(): Boolean 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/domain/state/StateEventManager.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.domain.state 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import com.codingwithmitch.cleannotes.util.EspressoIdlingResource 6 | import com.codingwithmitch.cleannotes.util.printLogD 7 | 8 | /** 9 | * - Keeps track of active StateEvents in DataChannelManager 10 | * - Keeps track of whether the progress bar should show or not based on a boolean 11 | * value in each StateEvent (shouldDisplayProgressBar) 12 | */ 13 | class StateEventManager { 14 | 15 | private val activeStateEvents: HashMap = HashMap() 16 | 17 | private val _shouldDisplayProgressBar: MutableLiveData = MutableLiveData() 18 | 19 | val shouldDisplayProgressBar: LiveData 20 | get() = _shouldDisplayProgressBar 21 | 22 | fun getActiveJobNames(): MutableSet{ 23 | return activeStateEvents.keys 24 | } 25 | 26 | fun clearActiveStateEventCounter(){ 27 | printLogD("DCM", "Clear active state events") 28 | EspressoIdlingResource.clear() 29 | activeStateEvents.clear() 30 | syncNumActiveStateEvents() 31 | } 32 | 33 | fun addStateEvent(stateEvent: StateEvent){ 34 | EspressoIdlingResource.increment() 35 | activeStateEvents.put(stateEvent.eventName(), stateEvent) 36 | syncNumActiveStateEvents() 37 | } 38 | 39 | fun removeStateEvent(stateEvent: StateEvent?){ 40 | printLogD("DCM sem", "remove state event: ${stateEvent?.eventName()}") 41 | stateEvent?.let { 42 | EspressoIdlingResource.decrement() 43 | } 44 | activeStateEvents.remove(stateEvent?.eventName()) 45 | syncNumActiveStateEvents() 46 | } 47 | 48 | fun isStateEventActive(stateEvent: StateEvent): Boolean{ 49 | printLogD("DCM sem", "is state event active? " + 50 | "${activeStateEvents.containsKey(stateEvent.eventName())}") 51 | return activeStateEvents.containsKey(stateEvent.eventName()) 52 | } 53 | 54 | private fun syncNumActiveStateEvents(){ 55 | var shouldDisplayProgressBar = false 56 | for(stateEvent in activeStateEvents.values){ 57 | if(stateEvent.shouldDisplayProgressBar()){ 58 | shouldDisplayProgressBar = true 59 | } 60 | } 61 | _shouldDisplayProgressBar.value = shouldDisplayProgressBar 62 | } 63 | } 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/domain/state/StateResource.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.domain.state 2 | 3 | import android.view.View 4 | import com.codingwithmitch.cleannotes.util.TodoCallback 5 | 6 | 7 | data class StateMessage(val response: Response) 8 | 9 | data class Response( 10 | val message: String?, 11 | val uiComponentType: UIComponentType, 12 | val messageType: MessageType 13 | ) 14 | 15 | sealed class UIComponentType{ 16 | 17 | class Toast: UIComponentType() 18 | 19 | class Dialog: UIComponentType() 20 | 21 | class AreYouSureDialog( 22 | val callback: AreYouSureCallback 23 | ): UIComponentType() 24 | 25 | class SnackBar( 26 | val undoCallback: SnackbarUndoCallback? = null, 27 | val onDismissCallback: TodoCallback? = null 28 | ): UIComponentType() 29 | 30 | class None: UIComponentType() 31 | } 32 | 33 | sealed class MessageType{ 34 | 35 | class Success: MessageType() 36 | 37 | class Error: MessageType() 38 | 39 | class Info: MessageType() 40 | 41 | class None: MessageType() 42 | } 43 | 44 | 45 | interface StateMessageCallback{ 46 | 47 | fun removeMessageFromStack() 48 | } 49 | 50 | 51 | interface AreYouSureCallback { 52 | 53 | fun proceed() 54 | 55 | fun cancel() 56 | } 57 | 58 | interface SnackbarUndoCallback { 59 | 60 | fun undo() 61 | } 62 | 63 | class SnackbarUndoListener 64 | constructor( 65 | private val snackbarUndoCallback: SnackbarUndoCallback? 66 | ): View.OnClickListener { 67 | 68 | override fun onClick(v: View?) { 69 | snackbarUndoCallback?.undo() 70 | } 71 | 72 | } 73 | 74 | 75 | interface DialogInputCaptureCallback { 76 | 77 | fun onTextCaptured(text: String) 78 | } 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/domain/state/ViewState.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.domain.state 2 | 3 | interface ViewState { 4 | 5 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/domain/util/DateUtil.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.domain.util 2 | 3 | import com.google.firebase.Timestamp 4 | import java.text.SimpleDateFormat 5 | import java.util.* 6 | import javax.inject.Singleton 7 | 8 | @Singleton 9 | class DateUtil 10 | constructor( 11 | private val dateFormat: SimpleDateFormat 12 | ) 13 | { 14 | 15 | fun removeTimeFromDateString(sd: String): String{ 16 | return sd.substring(0, sd.indexOf(" ")) 17 | } 18 | 19 | fun convertFirebaseTimestampToStringData(timestamp: Timestamp): String{ 20 | return dateFormat.format(timestamp.toDate()) 21 | } 22 | 23 | // Date format: "2019-07-23 HH:mm:ss" 24 | fun convertStringDateToFirebaseTimestamp(date: String): Timestamp{ 25 | return Timestamp(dateFormat.parse(date)) 26 | } 27 | 28 | // dates format looks like this: "2019-07-23 HH:mm:ss" 29 | fun getCurrentTimestamp(): String { 30 | return dateFormat.format(Date()) 31 | } 32 | 33 | } 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/domain/util/EntityMapper.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.domain.util 2 | 3 | interface EntityMapper { 4 | 5 | fun mapFromEntity(entity: Entity): DomainModel 6 | 7 | fun mapToEntity(domainModel: DomainModel): Entity 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/interactors/common/DeleteNote.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.interactors.common 2 | 3 | import com.codingwithmitch.cleannotes.business.data.cache.CacheResponseHandler 4 | import com.codingwithmitch.cleannotes.business.data.cache.abstraction.NoteCacheDataSource 5 | import com.codingwithmitch.cleannotes.business.data.network.abstraction.NoteNetworkDataSource 6 | import com.codingwithmitch.cleannotes.business.domain.model.Note 7 | import com.codingwithmitch.cleannotes.business.domain.state.* 8 | import com.codingwithmitch.cleannotes.business.data.util.safeApiCall 9 | import com.codingwithmitch.cleannotes.business.data.util.safeCacheCall 10 | import kotlinx.coroutines.Dispatchers.IO 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.flow.flow 13 | 14 | class DeleteNote( 15 | private val noteCacheDataSource: NoteCacheDataSource, 16 | private val noteNetworkDataSource: NoteNetworkDataSource 17 | ){ 18 | 19 | fun deleteNote( 20 | note: Note, 21 | stateEvent: StateEvent 22 | ): Flow?> = flow { 23 | 24 | val cacheResult = safeCacheCall(IO){ 25 | noteCacheDataSource.deleteNote(note.id) 26 | } 27 | 28 | val response = object: CacheResponseHandler( 29 | response = cacheResult, 30 | stateEvent = stateEvent 31 | ){ 32 | override suspend fun handleSuccess(resultObj: Int): DataState? { 33 | return if(resultObj > 0){ 34 | DataState.data( 35 | response = Response( 36 | message = DELETE_NOTE_SUCCESS, 37 | uiComponentType = UIComponentType.None(), 38 | messageType = MessageType.Success() 39 | ), 40 | data = null, 41 | stateEvent = stateEvent 42 | ) 43 | } 44 | else{ 45 | DataState.data( 46 | response = Response( 47 | message = DELETE_NOTE_FAILED, 48 | uiComponentType = UIComponentType.Toast(), 49 | messageType = MessageType.Error() 50 | ), 51 | data = null, 52 | stateEvent = stateEvent 53 | ) 54 | } 55 | } 56 | }.getResult() 57 | 58 | emit(response) 59 | 60 | // update network 61 | if(response?.stateMessage?.response?.message.equals(DELETE_NOTE_SUCCESS)){ 62 | 63 | // delete from 'notes' node 64 | safeApiCall(IO){ 65 | noteNetworkDataSource.deleteNote(note.id) 66 | } 67 | 68 | // insert into 'deletes' node 69 | safeApiCall(IO){ 70 | noteNetworkDataSource.insertDeletedNote(note) 71 | } 72 | 73 | } 74 | } 75 | 76 | companion object{ 77 | val DELETE_NOTE_SUCCESS = "Successfully deleted note." 78 | val DELETE_NOTE_PENDING = "Delete pending..." 79 | val DELETE_NOTE_FAILED = "Failed to delete note." 80 | val DELETE_ARE_YOU_SURE = "Are you sure you want to delete this?" 81 | } 82 | } 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/interactors/notedetail/NoteDetailInteractors.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.interactors.notedetail 2 | 3 | import com.codingwithmitch.cleannotes.business.interactors.common.DeleteNote 4 | import com.codingwithmitch.cleannotes.framework.presentation.notedetail.state.NoteDetailViewState 5 | 6 | // Use cases 7 | class NoteDetailInteractors ( 8 | val deleteNote: DeleteNote, 9 | val updateNote: UpdateNote 10 | ) 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/interactors/notedetail/UpdateNote.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.interactors.notedetail 2 | 3 | import com.codingwithmitch.cleannotes.business.data.cache.CacheResponseHandler 4 | import com.codingwithmitch.cleannotes.business.data.cache.abstraction.NoteCacheDataSource 5 | import com.codingwithmitch.cleannotes.business.data.network.abstraction.NoteNetworkDataSource 6 | import com.codingwithmitch.cleannotes.business.domain.model.Note 7 | import com.codingwithmitch.cleannotes.business.domain.state.* 8 | import com.codingwithmitch.cleannotes.business.data.util.safeApiCall 9 | import com.codingwithmitch.cleannotes.business.data.util.safeCacheCall 10 | import com.codingwithmitch.cleannotes.framework.presentation.notedetail.state.NoteDetailViewState 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.flow 14 | 15 | class UpdateNote( 16 | private val noteCacheDataSource: NoteCacheDataSource, 17 | private val noteNetworkDataSource: NoteNetworkDataSource 18 | ){ 19 | 20 | fun updateNote( 21 | note: Note, 22 | stateEvent: StateEvent 23 | ): Flow?> = flow { 24 | 25 | val cacheResult = safeCacheCall(Dispatchers.IO){ 26 | noteCacheDataSource.updateNote( 27 | primaryKey = note.id, 28 | newTitle = note.title, 29 | newBody = note.body, 30 | timestamp = null // generate new timestamp 31 | ) 32 | } 33 | 34 | val response = object: CacheResponseHandler( 35 | response = cacheResult, 36 | stateEvent = stateEvent 37 | ){ 38 | override suspend fun handleSuccess(resultObj: Int): DataState? { 39 | return if(resultObj > 0){ 40 | DataState.data( 41 | response = Response( 42 | message = UPDATE_NOTE_SUCCESS, 43 | uiComponentType = UIComponentType.Toast(), 44 | messageType = MessageType.Success() 45 | ), 46 | data = null, 47 | stateEvent = stateEvent 48 | ) 49 | } 50 | else{ 51 | DataState.data( 52 | response = Response( 53 | message = UPDATE_NOTE_FAILED, 54 | uiComponentType = UIComponentType.Toast(), 55 | messageType = MessageType.Error() 56 | ), 57 | data = null, 58 | stateEvent = stateEvent 59 | ) 60 | } 61 | } 62 | }.getResult() 63 | 64 | emit(response) 65 | 66 | updateNetwork(response?.stateMessage?.response?.message, note) 67 | } 68 | 69 | private suspend fun updateNetwork(response: String?, note: Note) { 70 | if(response.equals(UPDATE_NOTE_SUCCESS)){ 71 | 72 | safeApiCall(Dispatchers.IO){ 73 | noteNetworkDataSource.insertOrUpdateNote(note) 74 | } 75 | } 76 | } 77 | 78 | companion object{ 79 | val UPDATE_NOTE_SUCCESS = "Successfully updated note." 80 | val UPDATE_NOTE_FAILED = "Failed to update note." 81 | val UPDATE_NOTE_FAILED_PK = "Update failed. Note is missing primary key." 82 | 83 | } 84 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/interactors/notelist/GetNumNotes.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.interactors.notelist 2 | 3 | import com.codingwithmitch.cleannotes.business.data.cache.CacheResponseHandler 4 | import com.codingwithmitch.cleannotes.business.data.cache.abstraction.NoteCacheDataSource 5 | import com.codingwithmitch.cleannotes.business.domain.state.* 6 | import com.codingwithmitch.cleannotes.business.data.util.safeCacheCall 7 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.state.NoteListViewState 8 | import kotlinx.coroutines.Dispatchers.IO 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.flow 11 | 12 | class GetNumNotes( 13 | private val noteCacheDataSource: NoteCacheDataSource 14 | ){ 15 | 16 | fun getNumNotes( 17 | stateEvent: StateEvent 18 | ): Flow?> = flow { 19 | 20 | val cacheResult = safeCacheCall(IO){ 21 | noteCacheDataSource.getNumNotes() 22 | } 23 | val response = object: CacheResponseHandler( 24 | response = cacheResult, 25 | stateEvent = stateEvent 26 | ){ 27 | override suspend fun handleSuccess(resultObj: Int): DataState? { 28 | val viewState = NoteListViewState( 29 | numNotesInCache = resultObj 30 | ) 31 | return DataState.data( 32 | response = Response( 33 | message = GET_NUM_NOTES_SUCCESS, 34 | uiComponentType = UIComponentType.None(), 35 | messageType = MessageType.Success() 36 | ), 37 | data = viewState, 38 | stateEvent = stateEvent 39 | ) 40 | } 41 | }.getResult() 42 | 43 | emit(response) 44 | } 45 | 46 | companion object{ 47 | val GET_NUM_NOTES_SUCCESS = "Successfully retrieved the number of notes from the cache." 48 | val GET_NUM_NOTES_FAILED = "Failed to get the number of notes from the cache." 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/interactors/notelist/InsertMultipleNotes.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.interactors.notelist 2 | 3 | import com.codingwithmitch.cleannotes.business.data.cache.abstraction.NoteCacheDataSource 4 | import com.codingwithmitch.cleannotes.business.data.network.abstraction.NoteNetworkDataSource 5 | import com.codingwithmitch.cleannotes.business.domain.model.Note 6 | import com.codingwithmitch.cleannotes.business.domain.state.* 7 | import com.codingwithmitch.cleannotes.business.domain.util.DateUtil 8 | import com.codingwithmitch.cleannotes.business.data.util.safeApiCall 9 | import com.codingwithmitch.cleannotes.business.data.util.safeCacheCall 10 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.state.NoteListViewState 11 | import kotlinx.coroutines.Dispatchers.IO 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.flow 14 | import java.text.SimpleDateFormat 15 | import java.util.* 16 | import kotlin.collections.ArrayList 17 | 18 | // For testing 19 | class InsertMultipleNotes( 20 | private val noteCacheDataSource: NoteCacheDataSource, 21 | private val noteNetworkDataSource: NoteNetworkDataSource 22 | ){ 23 | 24 | fun insertNotes( 25 | numNotes: Int, 26 | stateEvent: StateEvent 27 | ): Flow?> = flow { 28 | 29 | val noteList = NoteListTester.generateNoteList(numNotes) 30 | safeCacheCall(IO){ 31 | noteCacheDataSource.insertNotes(noteList) 32 | } 33 | 34 | emit( 35 | DataState.data( 36 | response = Response( 37 | message = "success", 38 | uiComponentType = UIComponentType.None(), 39 | messageType = MessageType.None() 40 | ), 41 | data = null, 42 | stateEvent = stateEvent 43 | ) 44 | ) 45 | 46 | updateNetwork(noteList) 47 | } 48 | 49 | private suspend fun updateNetwork(noteList: List){ 50 | safeApiCall(IO){ 51 | noteNetworkDataSource.insertOrUpdateNotes(noteList) 52 | } 53 | } 54 | 55 | } 56 | 57 | 58 | private object NoteListTester { 59 | 60 | private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) 61 | private val dateUtil = 62 | DateUtil(dateFormat) 63 | 64 | fun generateNoteList(numNotes: Int): List{ 65 | val list: ArrayList = ArrayList() 66 | for(id in 0..numNotes){ 67 | list.add(generateNote()) 68 | } 69 | return list 70 | } 71 | 72 | fun generateNote(): Note { 73 | val note = Note( 74 | id = UUID.randomUUID().toString(), 75 | title = UUID.randomUUID().toString(), 76 | body = UUID.randomUUID().toString(), 77 | created_at = dateUtil.getCurrentTimestamp(), 78 | updated_at = dateUtil.getCurrentTimestamp() 79 | ) 80 | return note 81 | } 82 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/interactors/notelist/NoteListInteractors.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.interactors.notelist 2 | 3 | import com.codingwithmitch.cleannotes.business.interactors.common.DeleteNote 4 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.state.NoteListViewState 5 | 6 | 7 | // Use cases 8 | class NoteListInteractors ( 9 | val insertNewNote: InsertNewNote, 10 | val deleteNote: DeleteNote, 11 | val searchNotes: SearchNotes, 12 | val getNumNotes: GetNumNotes, 13 | val restoreDeletedNote: RestoreDeletedNote, 14 | val deleteMultipleNotes: DeleteMultipleNotes, 15 | val insertMultipleNotes: InsertMultipleNotes // for testing 16 | ) 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/interactors/notelist/SearchNotes.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.interactors.notelist 2 | 3 | import com.codingwithmitch.cleannotes.business.data.cache.CacheResponseHandler 4 | import com.codingwithmitch.cleannotes.business.data.cache.abstraction.NoteCacheDataSource 5 | import com.codingwithmitch.cleannotes.business.domain.model.Note 6 | import com.codingwithmitch.cleannotes.business.domain.state.* 7 | import com.codingwithmitch.cleannotes.business.data.util.safeCacheCall 8 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.state.NoteListViewState 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.flow.flow 12 | 13 | class SearchNotes( 14 | private val noteCacheDataSource: NoteCacheDataSource 15 | ){ 16 | 17 | fun searchNotes( 18 | query: String, 19 | filterAndOrder: String, 20 | page: Int, 21 | stateEvent: StateEvent 22 | ): Flow?> = flow { 23 | var updatedPage = page 24 | if(page <= 0){ 25 | updatedPage = 1 26 | } 27 | val cacheResult = safeCacheCall(Dispatchers.IO){ 28 | noteCacheDataSource.searchNotes( 29 | query = query, 30 | filterAndOrder = filterAndOrder, 31 | page = updatedPage 32 | ) 33 | } 34 | 35 | val response = object: CacheResponseHandler>( 36 | response = cacheResult, 37 | stateEvent = stateEvent 38 | ){ 39 | override suspend fun handleSuccess(resultObj: List): DataState? { 40 | var message: String? = 41 | SEARCH_NOTES_SUCCESS 42 | var uiComponentType: UIComponentType? = UIComponentType.None() 43 | if(resultObj.size == 0){ 44 | message = 45 | SEARCH_NOTES_NO_MATCHING_RESULTS 46 | uiComponentType = UIComponentType.Toast() 47 | } 48 | return DataState.data( 49 | response = Response( 50 | message = message, 51 | uiComponentType = uiComponentType as UIComponentType, 52 | messageType = MessageType.Success() 53 | ), 54 | data = NoteListViewState( 55 | noteList = ArrayList(resultObj) 56 | ), 57 | stateEvent = stateEvent 58 | ) 59 | } 60 | }.getResult() 61 | 62 | emit(response) 63 | } 64 | 65 | companion object{ 66 | val SEARCH_NOTES_SUCCESS = "Successfully retrieved list of notes." 67 | val SEARCH_NOTES_NO_MATCHING_RESULTS = "There are no notes that match that query." 68 | val SEARCH_NOTES_FAILED = "Failed to retrieve the list of notes." 69 | 70 | } 71 | } 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/business/interactors/splash/SyncDeletedNotes.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.interactors.splash 2 | 3 | import com.codingwithmitch.cleannotes.business.data.cache.CacheResponseHandler 4 | import com.codingwithmitch.cleannotes.business.data.cache.abstraction.NoteCacheDataSource 5 | import com.codingwithmitch.cleannotes.business.data.network.ApiResponseHandler 6 | import com.codingwithmitch.cleannotes.business.data.network.abstraction.NoteNetworkDataSource 7 | import com.codingwithmitch.cleannotes.business.domain.model.Note 8 | import com.codingwithmitch.cleannotes.business.domain.state.DataState 9 | import com.codingwithmitch.cleannotes.business.data.util.safeApiCall 10 | import com.codingwithmitch.cleannotes.business.data.util.safeCacheCall 11 | import com.codingwithmitch.cleannotes.util.printLogD 12 | import kotlinx.coroutines.Dispatchers.IO 13 | 14 | /* 15 | Search firestore for all notes in the "deleted" node. 16 | It will then search the cache for notes matching those deleted notes. 17 | If a match is found, it is deleted from the cache. 18 | */ 19 | class SyncDeletedNotes( 20 | private val noteCacheDataSource: NoteCacheDataSource, 21 | private val noteNetworkDataSource: NoteNetworkDataSource 22 | ){ 23 | 24 | suspend fun syncDeletedNotes(){ 25 | 26 | val apiResult = safeApiCall(IO){ 27 | noteNetworkDataSource.getDeletedNotes() 28 | } 29 | val response = object: ApiResponseHandler, List>( 30 | response = apiResult, 31 | stateEvent = null 32 | ){ 33 | override suspend fun handleSuccess(resultObj: List): DataState>? { 34 | return DataState.data( 35 | response = null, 36 | data = resultObj, 37 | stateEvent = null 38 | ) 39 | } 40 | } 41 | 42 | val notes = response.getResult()?.data?: ArrayList() 43 | 44 | val cacheResult = safeCacheCall(IO){ 45 | noteCacheDataSource.deleteNotes(notes) 46 | } 47 | 48 | object: CacheResponseHandler( 49 | response = cacheResult, 50 | stateEvent = null 51 | ){ 52 | override suspend fun handleSuccess(resultObj: Int): DataState? { 53 | printLogD("SyncNotes", 54 | "num deleted notes: ${resultObj}") 55 | return DataState.data( 56 | response = null, 57 | data = resultObj, 58 | stateEvent = null 59 | ) 60 | } 61 | }.getResult() 62 | 63 | } 64 | 65 | 66 | } 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/di/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.di 2 | 3 | import com.codingwithmitch.cleannotes.framework.presentation.BaseApplication 4 | import com.codingwithmitch.cleannotes.framework.presentation.MainActivity 5 | import com.codingwithmitch.cleannotes.framework.presentation.splash.NoteNetworkSyncManager 6 | import com.codingwithmitch.cleannotes.framework.presentation.notedetail.NoteDetailFragment 7 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.NoteListFragment 8 | import com.codingwithmitch.cleannotes.framework.presentation.splash.SplashFragment 9 | import com.codingwithmitch.cleannotes.notes.di.NoteViewModelModule 10 | import dagger.BindsInstance 11 | import dagger.Component 12 | import kotlinx.coroutines.ExperimentalCoroutinesApi 13 | import kotlinx.coroutines.FlowPreview 14 | import javax.inject.Singleton 15 | 16 | @ExperimentalCoroutinesApi 17 | @FlowPreview 18 | @Singleton 19 | @Component( 20 | modules = [ 21 | ProductionModule::class, 22 | AppModule::class, 23 | NoteViewModelModule::class, 24 | NoteFragmentFactoryModule::class 25 | ] 26 | ) 27 | interface AppComponent { 28 | 29 | val noteNetworkSync: NoteNetworkSyncManager 30 | 31 | @Component.Factory 32 | interface Factory{ 33 | 34 | fun create(@BindsInstance app: BaseApplication): AppComponent 35 | } 36 | 37 | fun inject(mainActivity: MainActivity) 38 | 39 | fun inject(splashFragment: SplashFragment) 40 | 41 | fun inject(noteListFragment: NoteListFragment) 42 | 43 | fun inject(noteDetailFragment: NoteDetailFragment) 44 | } 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/di/NoteFragmentFactoryModule.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.di 2 | 3 | import androidx.fragment.app.FragmentFactory 4 | import androidx.lifecycle.ViewModelProvider 5 | import com.codingwithmitch.cleannotes.business.domain.util.DateUtil 6 | import com.codingwithmitch.cleannotes.framework.presentation.common.NoteFragmentFactory 7 | import dagger.Module 8 | import dagger.Provides 9 | import kotlinx.coroutines.ExperimentalCoroutinesApi 10 | import kotlinx.coroutines.FlowPreview 11 | import javax.inject.Singleton 12 | 13 | @FlowPreview 14 | @ExperimentalCoroutinesApi 15 | @Module 16 | object NoteFragmentFactoryModule { 17 | 18 | @JvmStatic 19 | @Singleton 20 | @Provides 21 | fun provideNoteFragmentFactory( 22 | viewModelFactory: ViewModelProvider.Factory, 23 | dateUtil: DateUtil 24 | ): FragmentFactory { 25 | return NoteFragmentFactory( 26 | viewModelFactory, 27 | dateUtil 28 | ) 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/di/NoteViewModelModule.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.notes.di 2 | 3 | import android.content.SharedPreferences 4 | import androidx.lifecycle.ViewModelProvider 5 | import com.codingwithmitch.cleannotes.business.domain.model.NoteFactory 6 | import com.codingwithmitch.cleannotes.business.interactors.notedetail.NoteDetailInteractors 7 | import com.codingwithmitch.cleannotes.business.interactors.notelist.NoteListInteractors 8 | import com.codingwithmitch.cleannotes.framework.presentation.common.NoteViewModelFactory 9 | import com.codingwithmitch.cleannotes.framework.presentation.splash.NoteNetworkSyncManager 10 | import dagger.Module 11 | import dagger.Provides 12 | import kotlinx.coroutines.ExperimentalCoroutinesApi 13 | import kotlinx.coroutines.FlowPreview 14 | import javax.inject.Singleton 15 | 16 | @ExperimentalCoroutinesApi 17 | @FlowPreview 18 | @Module 19 | object NoteViewModelModule { 20 | 21 | @Singleton 22 | @JvmStatic 23 | @Provides 24 | fun provideNoteViewModelFactory( 25 | noteListInteractors: NoteListInteractors, 26 | noteDetailInteractors: NoteDetailInteractors, 27 | noteNetworkSyncManager: NoteNetworkSyncManager, 28 | noteFactory: NoteFactory, 29 | editor: SharedPreferences.Editor, 30 | sharedPreferences: SharedPreferences 31 | ): ViewModelProvider.Factory{ 32 | return NoteViewModelFactory( 33 | noteListInteractors = noteListInteractors, 34 | noteDetailInteractors = noteDetailInteractors, 35 | noteNetworkSyncManager = noteNetworkSyncManager, 36 | noteFactory = noteFactory, 37 | editor = editor, 38 | sharedPreferences = sharedPreferences 39 | ) 40 | } 41 | 42 | } 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/di/ProductionModule.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.di 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import androidx.room.Room 6 | import com.codingwithmitch.cleannotes.framework.datasource.cache.database.NoteDatabase 7 | import com.codingwithmitch.cleannotes.framework.datasource.preferences.PreferenceKeys 8 | import com.codingwithmitch.cleannotes.framework.presentation.BaseApplication 9 | import com.codingwithmitch.cleannotes.util.AndroidTestUtils 10 | import com.google.firebase.firestore.FirebaseFirestore 11 | import dagger.Module 12 | import dagger.Provides 13 | import kotlinx.coroutines.ExperimentalCoroutinesApi 14 | import kotlinx.coroutines.FlowPreview 15 | import javax.inject.Singleton 16 | 17 | 18 | /* 19 | Dependencies in this class have test fakes for ui tests. See "TestModule.kt" in 20 | androidTest dir 21 | */ 22 | @ExperimentalCoroutinesApi 23 | @FlowPreview 24 | @Module 25 | object ProductionModule { 26 | 27 | @JvmStatic 28 | @Singleton 29 | @Provides 30 | fun provideAndroidTestUtils(): AndroidTestUtils { 31 | return AndroidTestUtils(false) 32 | } 33 | 34 | @JvmStatic 35 | @Singleton 36 | @Provides 37 | fun provideSharedPreferences( 38 | application: BaseApplication 39 | ): SharedPreferences { 40 | return application 41 | .getSharedPreferences( 42 | PreferenceKeys.NOTE_PREFERENCES, 43 | Context.MODE_PRIVATE 44 | ) 45 | } 46 | 47 | @JvmStatic 48 | @Singleton 49 | @Provides 50 | fun provideNoteDb(app: BaseApplication): NoteDatabase { 51 | return Room 52 | .databaseBuilder(app, NoteDatabase::class.java, NoteDatabase.DATABASE_NAME) 53 | .fallbackToDestructiveMigration() 54 | .build() 55 | } 56 | 57 | @JvmStatic 58 | @Singleton 59 | @Provides 60 | fun provideFirebaseFirestore(): FirebaseFirestore { 61 | return FirebaseFirestore.getInstance() 62 | } 63 | 64 | 65 | } 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/datasource/cache/abstraction/NoteDaoService.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.datasource.cache.abstraction 2 | 3 | import com.codingwithmitch.cleannotes.business.domain.model.Note 4 | import com.codingwithmitch.cleannotes.framework.datasource.cache.database.NOTE_PAGINATION_PAGE_SIZE 5 | 6 | interface NoteDaoService { 7 | 8 | suspend fun insertNote(note: Note): Long 9 | 10 | suspend fun insertNotes(notes: List): LongArray 11 | 12 | suspend fun searchNoteById(id: String): Note? 13 | 14 | suspend fun updateNote( 15 | primaryKey: String, 16 | title: String, 17 | body: String?, 18 | timestamp: String? 19 | ): Int 20 | 21 | suspend fun deleteNote(primaryKey: String): Int 22 | 23 | suspend fun deleteNotes(notes: List): Int 24 | 25 | suspend fun searchNotes(): List 26 | 27 | suspend fun getAllNotes(): List 28 | 29 | suspend fun searchNotesOrderByDateDESC( 30 | query: String, 31 | page: Int, 32 | pageSize: Int = NOTE_PAGINATION_PAGE_SIZE 33 | ): List 34 | 35 | suspend fun searchNotesOrderByDateASC( 36 | query: String, 37 | page: Int, 38 | pageSize: Int = NOTE_PAGINATION_PAGE_SIZE 39 | ): List 40 | 41 | suspend fun searchNotesOrderByTitleDESC( 42 | query: String, 43 | page: Int, 44 | pageSize: Int = NOTE_PAGINATION_PAGE_SIZE 45 | ): List 46 | 47 | suspend fun searchNotesOrderByTitleASC( 48 | query: String, 49 | page: Int, 50 | pageSize: Int = NOTE_PAGINATION_PAGE_SIZE 51 | ): List 52 | 53 | suspend fun getNumNotes(): Int 54 | 55 | suspend fun returnOrderedQuery( 56 | query: String, 57 | filterAndOrder: String, 58 | page: Int 59 | ): List 60 | } 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/datasource/cache/database/NoteDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.datasource.cache.database 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import com.codingwithmitch.cleannotes.framework.datasource.cache.model.NoteCacheEntity 6 | 7 | @Database(entities = [NoteCacheEntity::class ], version = 1) 8 | abstract class NoteDatabase: RoomDatabase() { 9 | 10 | abstract fun noteDao(): NoteDao 11 | 12 | companion object{ 13 | val DATABASE_NAME: String = "note_db" 14 | } 15 | 16 | 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/datasource/cache/mappers/CacheMapper.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.datasource.cache.mappers 2 | 3 | import com.codingwithmitch.cleannotes.business.domain.model.Note 4 | import com.codingwithmitch.cleannotes.business.domain.util.DateUtil 5 | import com.codingwithmitch.cleannotes.business.domain.util.EntityMapper 6 | import com.codingwithmitch.cleannotes.framework.datasource.cache.model.NoteCacheEntity 7 | import javax.inject.Inject 8 | 9 | /** 10 | * Maps Note to NoteCacheEntity or NoteCacheEntity to Note. 11 | */ 12 | class CacheMapper 13 | @Inject 14 | constructor( 15 | private val dateUtil: DateUtil 16 | ): EntityMapper 17 | { 18 | 19 | fun entityListToNoteList(entities: List): List{ 20 | val list: ArrayList = ArrayList() 21 | for(entity in entities){ 22 | list.add(mapFromEntity(entity)) 23 | } 24 | return list 25 | } 26 | 27 | fun noteListToEntityList(notes: List): List{ 28 | val entities: ArrayList = ArrayList() 29 | for(note in notes){ 30 | entities.add(mapToEntity(note)) 31 | } 32 | return entities 33 | } 34 | 35 | override fun mapFromEntity(entity: NoteCacheEntity): Note { 36 | return Note( 37 | id = entity.id, 38 | title = entity.title, 39 | body = entity.body, 40 | updated_at = entity.updated_at, 41 | created_at = entity.created_at 42 | ) 43 | } 44 | 45 | override fun mapToEntity(domainModel: Note): NoteCacheEntity { 46 | return NoteCacheEntity( 47 | id = domainModel.id, 48 | title = domainModel.title, 49 | body = domainModel.body, 50 | updated_at = domainModel.updated_at, 51 | created_at = domainModel.created_at 52 | ) 53 | } 54 | } 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/datasource/cache/model/NoteCacheEntity.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.datasource.cache.model 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity(tableName = "notes") 8 | data class NoteCacheEntity( 9 | 10 | @PrimaryKey(autoGenerate = false) 11 | @ColumnInfo(name = "id") 12 | var id: String, 13 | 14 | @ColumnInfo(name = "title") 15 | var title: String, 16 | 17 | @ColumnInfo(name = "body") 18 | var body: String, 19 | 20 | @ColumnInfo(name = "updated_at") 21 | var updated_at: String, 22 | 23 | @ColumnInfo(name = "created_at") 24 | var created_at: String 25 | 26 | ){ 27 | 28 | 29 | 30 | companion object{ 31 | 32 | fun nullTitleError(): String{ 33 | return "You must enter a title." 34 | } 35 | 36 | fun nullIdError(): String{ 37 | return "NoteEntity object has a null id. This should not be possible. Check local database." 38 | } 39 | } 40 | 41 | override fun equals(other: Any?): Boolean { 42 | if (this === other) return true 43 | if (javaClass != other?.javaClass) return false 44 | 45 | other as NoteCacheEntity 46 | 47 | if (id != other.id) return false 48 | if (title != other.title) return false 49 | if (body != other.body) return false 50 | // if (updated_at != other.updated_at) return false // ignore this 51 | if (created_at != other.created_at) return false 52 | 53 | return true 54 | } 55 | 56 | override fun hashCode(): Int { 57 | var result = id.hashCode() 58 | result = 31 * result + title.hashCode() 59 | result = 31 * result + body.hashCode() 60 | result = 31 * result + updated_at.hashCode() 61 | result = 31 * result + created_at.hashCode() 62 | return result 63 | } 64 | } 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/datasource/network/abstraction/NoteFirestoreService.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.datasource.network.abstraction 2 | 3 | import com.codingwithmitch.cleannotes.business.domain.model.Note 4 | 5 | interface NoteFirestoreService { 6 | 7 | suspend fun insertOrUpdateNote(note: Note) 8 | 9 | suspend fun insertOrUpdateNotes(notes: List) 10 | 11 | suspend fun deleteNote(primaryKey: String) 12 | 13 | suspend fun insertDeletedNote(note: Note) 14 | 15 | suspend fun insertDeletedNotes(notes: List) 16 | 17 | suspend fun deleteDeletedNote(note: Note) 18 | 19 | suspend fun deleteAllNotes() 20 | 21 | suspend fun getDeletedNotes(): List 22 | 23 | suspend fun searchNote(note: Note): Note? 24 | 25 | suspend fun getAllNotes(): List 26 | 27 | 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/datasource/network/mappers/NetworkMapper.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.datasource.network.mappers 2 | 3 | import com.codingwithmitch.cleannotes.business.domain.model.Note 4 | import com.codingwithmitch.cleannotes.business.domain.util.DateUtil 5 | import com.codingwithmitch.cleannotes.business.domain.util.EntityMapper 6 | import com.codingwithmitch.cleannotes.framework.datasource.network.model.NoteNetworkEntity 7 | import javax.inject.Inject 8 | 9 | /** 10 | * Maps Note to NoteNetworkEntity or NoteNetworkEntity to Note. 11 | */ 12 | class NetworkMapper 13 | @Inject 14 | constructor( 15 | private val dateUtil: DateUtil 16 | ): EntityMapper 17 | { 18 | 19 | fun entityListToNoteList(entities: List): List{ 20 | val list: ArrayList = ArrayList() 21 | for(entity in entities){ 22 | list.add(mapFromEntity(entity)) 23 | } 24 | return list 25 | } 26 | 27 | fun noteListToEntityList(notes: List): List{ 28 | val entities: ArrayList = ArrayList() 29 | for(note in notes){ 30 | entities.add(mapToEntity(note)) 31 | } 32 | return entities 33 | } 34 | 35 | override fun mapFromEntity(entity: NoteNetworkEntity): Note { 36 | return Note( 37 | id = entity.id, 38 | title = entity.title, 39 | body = entity.body, 40 | updated_at = dateUtil.convertFirebaseTimestampToStringData(entity.updated_at), 41 | created_at = dateUtil.convertFirebaseTimestampToStringData(entity.created_at) 42 | ) 43 | } 44 | 45 | override fun mapToEntity(domainModel: Note): NoteNetworkEntity { 46 | return NoteNetworkEntity( 47 | id = domainModel.id, 48 | title = domainModel.title, 49 | body = domainModel.body, 50 | updated_at = dateUtil.convertStringDateToFirebaseTimestamp(domainModel.updated_at), 51 | created_at = dateUtil.convertStringDateToFirebaseTimestamp(domainModel.created_at) 52 | ) 53 | } 54 | 55 | 56 | } 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/datasource/network/model/NoteNetworkEntity.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.datasource.network.model 2 | 3 | import com.google.firebase.Timestamp 4 | 5 | 6 | data class NoteNetworkEntity( 7 | 8 | var id: String, 9 | 10 | var title: String, 11 | 12 | var body: String, 13 | 14 | var updated_at: Timestamp, 15 | 16 | var created_at: Timestamp 17 | 18 | ){ 19 | 20 | // no arg constructor for firestore 21 | constructor(): this( 22 | "", 23 | "", 24 | "", 25 | Timestamp.now(), 26 | Timestamp.now() 27 | ) 28 | 29 | 30 | 31 | companion object{ 32 | 33 | const val UPDATED_AT_FIELD = "updated_at" 34 | const val TITLE_FIELD = "title" 35 | const val BODY_FIELD = "body" 36 | } 37 | 38 | override fun equals(other: Any?): Boolean { 39 | if (this === other) return true 40 | if (javaClass != other?.javaClass) return false 41 | 42 | other as NoteNetworkEntity 43 | 44 | if (id != other.id) return false 45 | if (title != other.title) return false 46 | if (body != other.body) return false 47 | // if (updated_at != other.updated_at) return false // ignore 48 | if (created_at != other.created_at) return false 49 | 50 | return true 51 | } 52 | 53 | override fun hashCode(): Int { 54 | var result = id.hashCode() 55 | result = 31 * result + title.hashCode() 56 | result = 31 * result + body.hashCode() 57 | result = 31 * result + updated_at.hashCode() 58 | result = 31 * result + created_at.hashCode() 59 | return result 60 | } 61 | } 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/datasource/preferences/PreferenceKeys.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.datasource.preferences 2 | 3 | class PreferenceKeys { 4 | 5 | companion object{ 6 | 7 | // Shared Preference Files: 8 | const val NOTE_PREFERENCES: String = "com.codingwithmitch.cleannotes.notes" 9 | 10 | // Shared Preference Keys 11 | val NOTE_FILTER: String = "${NOTE_PREFERENCES}.NOTE_FILTER" 12 | val NOTE_ORDER: String = "${NOTE_PREFERENCES}.NOTE_ORDER" 13 | 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/BaseApplication.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.presentation 2 | 3 | import android.app.Application 4 | import com.codingwithmitch.cleannotes.di.AppComponent 5 | import com.codingwithmitch.cleannotes.di.DaggerAppComponent 6 | import kotlinx.coroutines.* 7 | 8 | @FlowPreview 9 | @ExperimentalCoroutinesApi 10 | open class BaseApplication : Application(){ 11 | 12 | lateinit var appComponent: AppComponent 13 | 14 | override fun onCreate() { 15 | super.onCreate() 16 | initAppComponent() 17 | } 18 | 19 | open fun initAppComponent(){ 20 | appComponent = DaggerAppComponent 21 | .factory() 22 | .create(this) 23 | } 24 | 25 | 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/UIController.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.presentation 2 | 3 | import com.codingwithmitch.cleannotes.business.domain.state.DialogInputCaptureCallback 4 | import com.codingwithmitch.cleannotes.business.domain.state.Response 5 | import com.codingwithmitch.cleannotes.business.domain.state.StateMessageCallback 6 | 7 | 8 | interface UIController { 9 | 10 | fun displayProgressBar(isDisplayed: Boolean) 11 | 12 | fun hideSoftKeyboard() 13 | 14 | fun displayInputCaptureDialog(title: String, callback: DialogInputCaptureCallback) 15 | 16 | fun onResponseReceived( 17 | response: Response, 18 | stateMessageCallback: StateMessageCallback 19 | ) 20 | 21 | } 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/common/BaseNoteFragment.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.presentation.common 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import android.widget.TextView 9 | import androidx.annotation.LayoutRes 10 | import androidx.fragment.app.Fragment 11 | import com.codingwithmitch.cleannotes.di.AppComponent 12 | import com.codingwithmitch.cleannotes.framework.presentation.BaseApplication 13 | import com.codingwithmitch.cleannotes.framework.presentation.MainActivity 14 | import com.codingwithmitch.cleannotes.framework.presentation.UIController 15 | import com.codingwithmitch.cleannotes.util.TodoCallback 16 | import kotlinx.coroutines.ExperimentalCoroutinesApi 17 | import kotlinx.coroutines.FlowPreview 18 | import java.lang.ClassCastException 19 | 20 | @FlowPreview 21 | @ExperimentalCoroutinesApi 22 | abstract class BaseNoteFragment 23 | constructor( 24 | private @LayoutRes val layoutRes: Int 25 | ): Fragment() { 26 | 27 | lateinit var uiController: UIController 28 | 29 | override fun onCreateView( 30 | inflater: LayoutInflater, 31 | container: ViewGroup?, 32 | savedInstanceState: Bundle? 33 | ): View? { 34 | return inflater.inflate(layoutRes, container, false) 35 | } 36 | 37 | fun displayToolbarTitle(textView: TextView, title: String?, useAnimation: Boolean) { 38 | if(title != null){ 39 | showToolbarTitle(textView, title, useAnimation) 40 | } 41 | else{ 42 | hideToolbarTitle(textView, useAnimation) 43 | } 44 | } 45 | 46 | private fun hideToolbarTitle(textView: TextView, animation: Boolean){ 47 | if(animation){ 48 | textView.fadeOut( 49 | object: TodoCallback { 50 | override fun execute() { 51 | textView.text = "" 52 | } 53 | } 54 | ) 55 | } 56 | else{ 57 | textView.text = "" 58 | textView.gone() 59 | } 60 | } 61 | 62 | private fun showToolbarTitle( 63 | textView: TextView, 64 | title: String, 65 | animation: Boolean 66 | ){ 67 | textView.text = title 68 | if(animation){ 69 | textView.fadeIn() 70 | } 71 | else{ 72 | textView.visible() 73 | } 74 | } 75 | 76 | abstract fun inject() 77 | 78 | fun getAppComponent(): AppComponent{ 79 | return activity?.run { 80 | (application as BaseApplication).appComponent 81 | }?: throw Exception("AppComponent is null.") 82 | } 83 | 84 | override fun onAttach(context: Context) { 85 | inject() 86 | super.onAttach(context) 87 | setUIController(null) // null in production 88 | } 89 | 90 | fun setUIController(mockController: UIController?){ 91 | 92 | // TEST: Set interface from mock 93 | if(mockController != null){ 94 | this.uiController = mockController 95 | } 96 | else{ // PRODUCTION: if no mock, get from context 97 | activity?.let { 98 | if(it is MainActivity){ 99 | try{ 100 | uiController = context as UIController 101 | }catch (e: ClassCastException){ 102 | e.printStackTrace() 103 | } 104 | } 105 | } 106 | } 107 | } 108 | } 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/common/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.presentation.common 2 | 3 | import androidx.lifecycle.* 4 | import com.codingwithmitch.cleannotes.business.domain.state.* 5 | import com.codingwithmitch.cleannotes.business.data.util.GenericErrors 6 | import com.codingwithmitch.cleannotes.util.printLogD 7 | import kotlinx.coroutines.* 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.flow 10 | 11 | 12 | @FlowPreview 13 | @ExperimentalCoroutinesApi 14 | abstract class BaseViewModel : ViewModel() 15 | { 16 | private val _viewState: MutableLiveData = MutableLiveData() 17 | 18 | val dataChannelManager: DataChannelManager 19 | = object: DataChannelManager(){ 20 | 21 | override fun handleNewData(data: ViewState) { 22 | this@BaseViewModel.handleNewData(data) 23 | } 24 | } 25 | 26 | val viewState: LiveData 27 | get() = _viewState 28 | 29 | val shouldDisplayProgressBar: LiveData 30 | = dataChannelManager.shouldDisplayProgressBar 31 | 32 | val stateMessage: LiveData 33 | get() = dataChannelManager.messageStack.stateMessage 34 | 35 | // FOR DEBUGGING 36 | fun getMessageStackSize(): Int{ 37 | return dataChannelManager.messageStack.size 38 | } 39 | 40 | fun setupChannel() = dataChannelManager.setupChannel() 41 | 42 | abstract fun handleNewData(data: ViewState) 43 | 44 | abstract fun setStateEvent(stateEvent: StateEvent) 45 | 46 | fun emitStateMessageEvent( 47 | stateMessage: StateMessage, 48 | stateEvent: StateEvent 49 | ) = flow{ 50 | emit( 51 | DataState.error( 52 | response = stateMessage.response, 53 | stateEvent = stateEvent 54 | ) 55 | ) 56 | } 57 | 58 | fun emitInvalidStateEvent(stateEvent: StateEvent) = flow { 59 | emit( 60 | DataState.error( 61 | response = Response( 62 | message = GenericErrors.INVALID_STATE_EVENT, 63 | uiComponentType = UIComponentType.None(), 64 | messageType = MessageType.Error() 65 | ), 66 | stateEvent = stateEvent 67 | ) 68 | ) 69 | } 70 | 71 | fun launchJob( 72 | stateEvent: StateEvent, 73 | jobFunction: Flow?> 74 | ) = dataChannelManager.launchJob(stateEvent, jobFunction) 75 | 76 | fun getCurrentViewStateOrNew(): ViewState{ 77 | return viewState.value ?: initNewViewState() 78 | } 79 | 80 | fun setViewState(viewState: ViewState){ 81 | _viewState.value = viewState 82 | } 83 | 84 | fun clearStateMessage(index: Int = 0){ 85 | printLogD("BaseViewModel", "clearStateMessage") 86 | dataChannelManager.clearStateMessage(index) 87 | } 88 | 89 | fun clearActiveStateEvents() = dataChannelManager.clearActiveStateEventCounter() 90 | 91 | fun clearAllStateMessages() = dataChannelManager.clearAllStateMessages() 92 | 93 | fun printStateMessages() = dataChannelManager.printStateMessages() 94 | 95 | fun cancelActiveJobs() = dataChannelManager.cancelJobs() 96 | 97 | abstract fun initNewViewState(): ViewState 98 | 99 | } 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/common/KeyboardExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.presentation.common 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.view.View 6 | import android.view.inputmethod.InputMethodManager 7 | 8 | // Author: https://github.com/sanogueralorenzo/Android-Kotlin-Clean-Architecture 9 | /** 10 | * Use only from Activities, don't use from Fragment (with getActivity) or from Dialog/DialogFragment 11 | */ 12 | fun Activity.hideKeyboard() { 13 | val imm = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager 14 | val view = currentFocus ?: View(this) 15 | imm.hideSoftInputFromWindow(view.windowToken, 0) 16 | window.decorView 17 | } 18 | 19 | fun View.showKeyboard() { 20 | val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager 21 | imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY) 22 | } 23 | 24 | /** 25 | * Use everywhere except from Activity (Custom View, Fragment, Dialogs, DialogFragments). 26 | */ 27 | fun View.hideKeyboard() { 28 | val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager 29 | imm.hideSoftInputFromWindow(windowToken, 0) 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/common/NoteFragmentFactory.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.presentation.common 2 | 3 | import androidx.fragment.app.FragmentFactory 4 | import androidx.lifecycle.ViewModelProvider 5 | import com.codingwithmitch.cleannotes.business.domain.util.DateUtil 6 | import com.codingwithmitch.cleannotes.framework.presentation.notedetail.NoteDetailFragment 7 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.NoteListFragment 8 | import com.codingwithmitch.cleannotes.framework.presentation.splash.SplashFragment 9 | import kotlinx.coroutines.ExperimentalCoroutinesApi 10 | import kotlinx.coroutines.FlowPreview 11 | import javax.inject.Inject 12 | 13 | @ExperimentalCoroutinesApi 14 | @FlowPreview 15 | class NoteFragmentFactory 16 | @Inject 17 | constructor( 18 | private val viewModelFactory: ViewModelProvider.Factory, 19 | private val dateUtil: DateUtil 20 | ): FragmentFactory(){ 21 | 22 | override fun instantiate(classLoader: ClassLoader, className: String) = 23 | 24 | when(className){ 25 | 26 | NoteListFragment::class.java.name -> { 27 | val fragment = NoteListFragment(viewModelFactory, dateUtil) 28 | fragment 29 | } 30 | 31 | NoteDetailFragment::class.java.name -> { 32 | val fragment = NoteDetailFragment(viewModelFactory) 33 | fragment 34 | } 35 | 36 | SplashFragment::class.java.name -> { 37 | val fragment = SplashFragment(viewModelFactory) 38 | fragment 39 | } 40 | 41 | else -> { 42 | super.instantiate(classLoader, className) 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/common/NoteViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.presentation.common 2 | 3 | import android.content.SharedPreferences 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.ViewModelProvider 6 | import com.codingwithmitch.cleannotes.business.domain.model.NoteFactory 7 | import com.codingwithmitch.cleannotes.business.interactors.notedetail.NoteDetailInteractors 8 | import com.codingwithmitch.cleannotes.business.interactors.notelist.NoteListInteractors 9 | import com.codingwithmitch.cleannotes.framework.presentation.notedetail.NoteDetailViewModel 10 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.NoteListViewModel 11 | import com.codingwithmitch.cleannotes.framework.presentation.splash.NoteNetworkSyncManager 12 | import com.codingwithmitch.cleannotes.framework.presentation.splash.SplashViewModel 13 | import kotlinx.coroutines.ExperimentalCoroutinesApi 14 | import kotlinx.coroutines.FlowPreview 15 | import javax.inject.Inject 16 | import javax.inject.Singleton 17 | 18 | 19 | @FlowPreview 20 | @ExperimentalCoroutinesApi 21 | class NoteViewModelFactory 22 | constructor( 23 | private val noteListInteractors: NoteListInteractors, 24 | private val noteDetailInteractors: NoteDetailInteractors, 25 | private val noteNetworkSyncManager: NoteNetworkSyncManager, 26 | private val noteFactory: NoteFactory, 27 | private val editor: SharedPreferences.Editor, 28 | private val sharedPreferences: SharedPreferences 29 | ) : ViewModelProvider.Factory { 30 | 31 | override fun create(modelClass: Class): T { 32 | return when(modelClass){ 33 | 34 | NoteListViewModel::class.java -> { 35 | NoteListViewModel( 36 | noteInteractors = noteListInteractors, 37 | noteFactory = noteFactory, 38 | editor = editor, 39 | sharedPreferences = sharedPreferences 40 | ) as T 41 | } 42 | 43 | NoteDetailViewModel::class.java -> { 44 | NoteDetailViewModel( 45 | noteInteractors = noteDetailInteractors 46 | ) as T 47 | } 48 | 49 | SplashViewModel::class.java -> { 50 | SplashViewModel( 51 | noteNetworkSyncManager = noteNetworkSyncManager 52 | ) as T 53 | } 54 | 55 | else -> { 56 | throw IllegalArgumentException("unknown model class $modelClass") 57 | } 58 | } 59 | } 60 | } 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/common/TopSpacingItemDecoration.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.presentation.common 2 | 3 | import androidx.recyclerview.widget.RecyclerView 4 | import android.graphics.Rect 5 | import android.view.View 6 | 7 | 8 | class TopSpacingItemDecoration(private val padding: Int) : RecyclerView.ItemDecoration() { 9 | 10 | override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { 11 | super.getItemOffsets(outRect, view, parent, state) 12 | outRect.top = padding 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/notedetail/state/CollapsingToolbarState.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.presentation.notedetail.state 2 | 3 | 4 | sealed class CollapsingToolbarState{ 5 | 6 | class Collapsed: CollapsingToolbarState(){ 7 | 8 | override fun toString(): String { 9 | return "Collapsed" 10 | } 11 | } 12 | 13 | class Expanded: CollapsingToolbarState(){ 14 | 15 | override fun toString(): String { 16 | return "Expanded" 17 | } 18 | } 19 | } 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/notedetail/state/NoteDetailStateEvent.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.presentation.notedetail.state 2 | 3 | import com.codingwithmitch.cleannotes.business.domain.model.Note 4 | import com.codingwithmitch.cleannotes.business.domain.state.StateEvent 5 | import com.codingwithmitch.cleannotes.business.domain.state.StateMessage 6 | 7 | 8 | sealed class NoteDetailStateEvent: StateEvent { 9 | 10 | 11 | class UpdateNoteEvent: NoteDetailStateEvent(){ 12 | 13 | override fun errorInfo(): String { 14 | return "Error updating note." 15 | } 16 | 17 | override fun eventName(): String { 18 | return "UpdateNoteEvent" 19 | } 20 | 21 | override fun shouldDisplayProgressBar() = true 22 | } 23 | 24 | class DeleteNoteEvent( 25 | val note: Note 26 | ): NoteDetailStateEvent(){ 27 | 28 | override fun errorInfo(): String { 29 | return "Error deleting note." 30 | } 31 | 32 | override fun eventName(): String { 33 | return "DeleteNoteEvent" 34 | } 35 | 36 | override fun shouldDisplayProgressBar() = true 37 | } 38 | 39 | class CreateStateMessageEvent( 40 | val stateMessage: StateMessage 41 | ): NoteDetailStateEvent(){ 42 | 43 | override fun errorInfo(): String { 44 | return "Error creating a new state message." 45 | } 46 | 47 | override fun eventName(): String { 48 | return "CreateStateMessageEvent" 49 | } 50 | 51 | override fun shouldDisplayProgressBar() = false 52 | } 53 | 54 | } 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/notedetail/state/NoteDetailViewState.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.presentation.notedetail.state 2 | 3 | import android.os.Parcelable 4 | import com.codingwithmitch.cleannotes.business.domain.model.Note 5 | import com.codingwithmitch.cleannotes.business.domain.state.ViewState 6 | import kotlinx.android.parcel.Parcelize 7 | 8 | @Parcelize 9 | data class NoteDetailViewState( 10 | 11 | var note: Note? = null, 12 | 13 | var isUpdatePending: Boolean? = null 14 | 15 | ) : Parcelable, ViewState 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/notedetail/state/NoteInteractionManager.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.presentation.notedetail.state 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import com.codingwithmitch.cleannotes.framework.presentation.notedetail.state.CollapsingToolbarState.* 6 | import com.codingwithmitch.cleannotes.framework.presentation.notedetail.state.NoteInteractionState.* 7 | 8 | // Both can not be in 'EditState' at the same time. 9 | class NoteInteractionManager{ 10 | 11 | private val _noteTitleState: MutableLiveData 12 | = MutableLiveData(DefaultState()) 13 | 14 | private val _noteBodyState: MutableLiveData 15 | = MutableLiveData(DefaultState()) 16 | 17 | private val _collapsingToolbarState: MutableLiveData 18 | = MutableLiveData(Expanded()) 19 | 20 | val noteTitleState: LiveData 21 | get() = _noteTitleState 22 | 23 | val noteBodyState: LiveData 24 | get() = _noteBodyState 25 | 26 | val collapsingToolbarState: LiveData 27 | get() = _collapsingToolbarState 28 | 29 | fun setCollapsingToolbarState(state: CollapsingToolbarState){ 30 | if(!state.toString().equals(_collapsingToolbarState.value.toString())){ 31 | _collapsingToolbarState.value = state 32 | } 33 | } 34 | 35 | fun setNewNoteTitleState(state: NoteInteractionState){ 36 | if(!noteTitleState.toString().equals(state.toString())){ 37 | _noteTitleState.value = state 38 | when(state){ 39 | 40 | is EditState -> { 41 | _noteBodyState.value = DefaultState() 42 | } 43 | } 44 | } 45 | } 46 | 47 | fun setNewNoteBodyState(state: NoteInteractionState){ 48 | if(!noteBodyState.toString().equals(state.toString())){ 49 | _noteBodyState.value = state 50 | when(state){ 51 | 52 | is EditState -> { 53 | _noteTitleState.value = DefaultState() 54 | } 55 | } 56 | } 57 | } 58 | 59 | fun isEditingTitle() = noteTitleState.value.toString().equals(EditState().toString()) 60 | 61 | fun isEditingBody() = noteBodyState.value.toString().equals(EditState().toString()) 62 | 63 | fun exitEditState(){ 64 | _noteTitleState.value = DefaultState() 65 | _noteBodyState.value = DefaultState() 66 | } 67 | 68 | // return true if either title or body are in EditState 69 | fun checkEditState() = noteTitleState.value.toString().equals(EditState().toString()) 70 | || noteBodyState.value.toString().equals(EditState().toString()) 71 | 72 | 73 | 74 | } 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/notedetail/state/NoteInteractionState.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.presentation.notedetail.state 2 | 3 | 4 | 5 | sealed class NoteInteractionState { 6 | 7 | class EditState: NoteInteractionState() { 8 | 9 | override fun toString(): String { 10 | return "EditState" 11 | } 12 | } 13 | 14 | class DefaultState: NoteInteractionState(){ 15 | 16 | override fun toString(): String { 17 | return "DefaultState" 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/notelist/NoteItemTouchHelperCallback.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.presentation.notelist 2 | 3 | import androidx.recyclerview.widget.ItemTouchHelper 4 | import androidx.recyclerview.widget.RecyclerView 5 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.state.NoteListInteractionManager 6 | import kotlinx.coroutines.ExperimentalCoroutinesApi 7 | import kotlinx.coroutines.FlowPreview 8 | 9 | @ExperimentalCoroutinesApi 10 | @FlowPreview 11 | class NoteItemTouchHelperCallback 12 | constructor( 13 | private val itemTouchHelperAdapter: ItemTouchHelperAdapter, 14 | private val noteListInteractionManager: NoteListInteractionManager 15 | ): ItemTouchHelper.Callback() { 16 | 17 | override fun getMovementFlags( 18 | recyclerView: RecyclerView, 19 | viewHolder: RecyclerView.ViewHolder 20 | ): Int { 21 | return makeMovementFlags( 22 | ItemTouchHelper.ACTION_STATE_IDLE, 23 | ItemTouchHelper.START or ItemTouchHelper.END 24 | ) 25 | } 26 | 27 | override fun onMove( 28 | recyclerView: RecyclerView, 29 | viewHolder: RecyclerView.ViewHolder, 30 | target: RecyclerView.ViewHolder 31 | ): Boolean { 32 | return true 33 | } 34 | 35 | override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { 36 | itemTouchHelperAdapter.onItemSwiped(viewHolder.adapterPosition) 37 | } 38 | 39 | override fun isLongPressDragEnabled(): Boolean { 40 | return false 41 | } 42 | 43 | override fun isItemViewSwipeEnabled(): Boolean { 44 | return !noteListInteractionManager.isMultiSelectionStateActive() 45 | } 46 | 47 | } 48 | 49 | 50 | interface ItemTouchHelperAdapter{ 51 | 52 | fun onItemSwiped(position: Int) 53 | } 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/notelist/state/NoteListInteractionManager.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.presentation.notelist.state 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import com.codingwithmitch.cleannotes.business.domain.model.Note 6 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.state.NoteListToolbarState.* 7 | 8 | class NoteListInteractionManager { 9 | 10 | private val _selectedNotes: MutableLiveData> = MutableLiveData() 11 | 12 | private val _toolbarState: MutableLiveData 13 | = MutableLiveData(SearchViewState()) 14 | 15 | val selectedNotes: LiveData> 16 | get() = _selectedNotes 17 | 18 | val toolbarState: LiveData 19 | get() = _toolbarState 20 | 21 | fun setToolbarState(state: NoteListToolbarState){ 22 | _toolbarState.value = state 23 | } 24 | 25 | fun getSelectedNotes():ArrayList = _selectedNotes.value?: ArrayList() 26 | 27 | fun isMultiSelectionStateActive(): Boolean{ 28 | return _toolbarState.value.toString() == MultiSelectionState().toString() 29 | } 30 | 31 | fun addOrRemoveNoteFromSelectedList(note: Note){ 32 | var list = _selectedNotes.value 33 | if(list == null){ 34 | list = ArrayList() 35 | } 36 | if (list.contains(note)){ 37 | list.remove(note) 38 | } 39 | else{ 40 | list.add(note) 41 | } 42 | _selectedNotes.value = list 43 | } 44 | 45 | fun isNoteSelected(note: Note): Boolean{ 46 | return _selectedNotes.value?.contains(note)?: false 47 | } 48 | 49 | fun clearSelectedNotes(){ 50 | _selectedNotes.value = null 51 | } 52 | 53 | } 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/notelist/state/NoteListToolbarState.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.presentation.notelist.state 2 | 3 | sealed class NoteListToolbarState { 4 | 5 | class MultiSelectionState: NoteListToolbarState(){ 6 | 7 | override fun toString(): String { 8 | return "MultiSelectionState" 9 | } 10 | } 11 | 12 | class SearchViewState: NoteListToolbarState(){ 13 | 14 | override fun toString(): String { 15 | return "SearchViewState" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/notelist/state/NoteListViewState.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.presentation.notelist.state 2 | 3 | import android.os.Parcelable 4 | import com.codingwithmitch.cleannotes.business.domain.model.Note 5 | import com.codingwithmitch.cleannotes.business.domain.state.ViewState 6 | import kotlinx.android.parcel.Parcelize 7 | 8 | @Parcelize 9 | data class NoteListViewState( 10 | 11 | var noteList: ArrayList? = null, 12 | var newNote: Note? = null, // note that can be created with fab 13 | var notePendingDelete: NotePendingDelete? = null, // set when delete is pending (can be undone) 14 | var searchQuery: String? = null, 15 | var page: Int? = null, 16 | var isQueryExhausted: Boolean? = null, 17 | var filter: String? = null, 18 | var order: String? = null, 19 | var layoutManagerState: Parcelable? = null, 20 | var numNotesInCache: Int? = null 21 | 22 | ) : Parcelable, ViewState { 23 | 24 | @Parcelize 25 | data class NotePendingDelete( 26 | var note: Note? = null, 27 | var listPosition: Int? = null 28 | ) : Parcelable 29 | } 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/splash/NoteNetworkSyncManager.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.presentation.splash 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import com.codingwithmitch.cleannotes.business.interactors.splash.SyncDeletedNotes 6 | import com.codingwithmitch.cleannotes.business.interactors.splash.SyncNotes 7 | import com.codingwithmitch.cleannotes.util.printLogD 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.Dispatchers.Main 10 | import kotlinx.coroutines.launch 11 | import javax.inject.Inject 12 | import javax.inject.Singleton 13 | 14 | @Singleton 15 | class NoteNetworkSyncManager 16 | @Inject 17 | constructor( 18 | private val syncNotes: SyncNotes, 19 | private val syncDeletedNotes: SyncDeletedNotes 20 | ){ 21 | 22 | private val _hasSyncBeenExecuted: MutableLiveData = MutableLiveData(false) 23 | 24 | val hasSyncBeenExecuted: LiveData 25 | get() = _hasSyncBeenExecuted 26 | 27 | fun executeDataSync(coroutineScope: CoroutineScope){ 28 | if(_hasSyncBeenExecuted.value!!){ 29 | return 30 | } 31 | 32 | val syncJob = coroutineScope.launch { 33 | val deletesJob = launch { 34 | printLogD("SyncNotes", 35 | "syncing deleted notes.") 36 | syncDeletedNotes.syncDeletedNotes() 37 | } 38 | deletesJob.join() 39 | 40 | launch { 41 | printLogD("SyncNotes", 42 | "syncing notes.") 43 | syncNotes.syncNotes() 44 | } 45 | } 46 | syncJob.invokeOnCompletion { 47 | CoroutineScope(Main).launch{ 48 | _hasSyncBeenExecuted.value = true 49 | } 50 | } 51 | } 52 | 53 | } 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/splash/SplashFragment.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.presentation.splash 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.fragment.app.viewModels 6 | import androidx.lifecycle.Observer 7 | import androidx.lifecycle.ViewModelProvider 8 | import androidx.navigation.fragment.findNavController 9 | import com.codingwithmitch.cleannotes.R 10 | import com.codingwithmitch.cleannotes.business.domain.state.DialogInputCaptureCallback 11 | import com.codingwithmitch.cleannotes.framework.datasource.network.implementation.NoteFirestoreServiceImpl.Companion.EMAIL 12 | import com.codingwithmitch.cleannotes.framework.presentation.common.BaseNoteFragment 13 | import com.codingwithmitch.cleannotes.util.printLogD 14 | import com.google.firebase.auth.FirebaseAuth 15 | import kotlinx.coroutines.ExperimentalCoroutinesApi 16 | import kotlinx.coroutines.FlowPreview 17 | import javax.inject.Inject 18 | import javax.inject.Singleton 19 | 20 | @FlowPreview 21 | @ExperimentalCoroutinesApi 22 | @Singleton 23 | class SplashFragment 24 | @Inject 25 | constructor( 26 | private val viewModelFactory: ViewModelProvider.Factory 27 | ): BaseNoteFragment(R.layout.fragment_splash) { 28 | 29 | val viewModel: SplashViewModel by viewModels { 30 | viewModelFactory 31 | } 32 | 33 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 34 | super.onViewCreated(view, savedInstanceState) 35 | checkFirebaseAuth() 36 | } 37 | 38 | private fun checkFirebaseAuth(){ 39 | if(FirebaseAuth.getInstance().currentUser == null){ 40 | displayCapturePassword() 41 | } 42 | else{ 43 | subscribeObservers() 44 | } 45 | } 46 | 47 | // add password input b/c someone used my firestore and deleted the data 48 | private fun displayCapturePassword(){ 49 | uiController.displayInputCaptureDialog( 50 | getString(R.string.text_enter_password), 51 | object: DialogInputCaptureCallback { 52 | override fun onTextCaptured(text: String) { 53 | FirebaseAuth.getInstance() 54 | .signInWithEmailAndPassword(EMAIL, text) 55 | .addOnCompleteListener { 56 | if(it.isSuccessful){ 57 | printLogD("MainActivity", 58 | "Signing in to Firebase: ${it.result}") 59 | subscribeObservers() 60 | } 61 | else{ 62 | printLogD("MainActivity", "cannot log in") 63 | } 64 | } 65 | } 66 | } 67 | ) 68 | } 69 | 70 | private fun subscribeObservers(){ 71 | viewModel.hasSyncBeenExecuted().observe(viewLifecycleOwner, Observer { hasSyncBeenExecuted -> 72 | 73 | if(hasSyncBeenExecuted){ 74 | navNoteListFragment() 75 | } 76 | }) 77 | } 78 | 79 | private fun navNoteListFragment(){ 80 | findNavController().navigate(R.id.action_splashFragment_to_noteListFragment) 81 | } 82 | 83 | override fun inject() { 84 | getAppComponent().inject(this) 85 | } 86 | 87 | } 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/splash/SplashViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.framework.presentation.splash 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.asLiveData 6 | import androidx.lifecycle.viewModelScope 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.combine 9 | import kotlinx.coroutines.flow.flow 10 | import javax.inject.Inject 11 | import javax.inject.Singleton 12 | 13 | @Singleton 14 | class SplashViewModel 15 | @Inject 16 | constructor( 17 | private val noteNetworkSyncManager: NoteNetworkSyncManager 18 | ): ViewModel(){ 19 | 20 | init { 21 | syncCacheWithNetwork() 22 | } 23 | 24 | fun hasSyncBeenExecuted() = noteNetworkSyncManager.hasSyncBeenExecuted 25 | 26 | private fun syncCacheWithNetwork(){ 27 | noteNetworkSyncManager.executeDataSync(viewModelScope) 28 | } 29 | 30 | } 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/util/AndroidTestUtils.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.util 2 | 3 | import javax.inject.Inject 4 | import javax.inject.Singleton 5 | 6 | @Singleton 7 | class AndroidTestUtils 8 | @Inject 9 | constructor( 10 | private val isTest: Boolean 11 | ){ 12 | fun isTest() = isTest 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/util/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.util 2 | 3 | object Constants { 4 | 5 | const val TAG = "AppDebug" // Tag for logs 6 | const val DEBUG = true // enable logging 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/util/Logger.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.util 2 | 3 | import android.util.Log 4 | import com.codingwithmitch.cleannotes.util.Constants.DEBUG 5 | import com.codingwithmitch.cleannotes.util.Constants.TAG 6 | import com.google.firebase.crashlytics.FirebaseCrashlytics 7 | 8 | var isUnitTest = false 9 | 10 | fun printLogD(className: String?, message: String ) { 11 | if (DEBUG && !isUnitTest) { 12 | Log.d(TAG, "$className: $message") 13 | } 14 | else if(DEBUG && isUnitTest){ 15 | println("$className: $message") 16 | } 17 | } 18 | 19 | /* 20 | Priorities: Log.DEBUG, Log. etc.... 21 | */ 22 | fun cLog(msg: String?){ 23 | msg?.let { 24 | if(!DEBUG){ 25 | FirebaseCrashlytics.getInstance().log(it) 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/codingwithmitch/cleannotes/util/TodoCallback.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.util 2 | 3 | // simple callback to execute something after a function is called 4 | interface TodoCallback { 5 | 6 | fun execute() 7 | } -------------------------------------------------------------------------------- /app/src/main/res/anim/fade_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/anim/fade_out.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_in_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_in_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_out_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_out_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/color/bottom_nav_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_back_grey_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close_grey_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_date_range_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_done_grey_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_event_note_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_filter_list_grey_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_folder_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/list_item_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/logo_250_250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/app/src/main/res/drawable/logo_250_250.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/red_button_drawable.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 22 | 23 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_note_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 20 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 43 | 44 | 49 | 50 | 51 | 52 | 53 | 54 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_multiselection_toolbar.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | 14 | 23 | 24 | 41 | 42 | 43 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_note_detail_toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 18 | 19 | 36 | 37 | 38 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_note_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 18 | 19 | 33 | 34 | 35 | 36 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_searchview_toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 27 | 28 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_app_graph.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 21 | 22 | 23 | 27 | 34 | 35 | 36 | 40 | 41 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | #fff 6 | #7d7d7d 7 | #0094DE 8 | 9 | 10 | #0094DE 11 | #9FDAF7 12 | #0000EE 13 | #f2f2f2 14 | 15 | 16 | #e22b2b 17 | #fc8b8d 18 | #f2f2f2 19 | #c4c4c4 20 | #a8a8a8 21 | #7d7d7d 22 | #5c5c5c 23 | 24 | #686868 25 | #EEEEEE 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20sp 6 | 11sp 7 | 21sp 8 | 9 | 10 | 55dp 11 | 120dp 12 | 13 | 65dp 14 | 15 | 16dp 16 | 16dp 17 | -16dp 18 | 24dp 19 | 32dp 20 | 7dp 21 | 5dp 22 | 23 | 80dp 24 | 25 | 80dp 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Clean Notes 3 | DynamicFeature 4 | 5 | Notes module 6 | Reminders module 7 | Settings module 8 | 9 | 10 | notes 11 | settings 12 | reminders 13 | 14 | Error 15 | Success 16 | OK 17 | Cancel 18 | Info 19 | Are you sure? 20 | Yes 21 | Enter a title 22 | Undo 23 | 24 | 25 | Notes go here 26 | Due Date... 27 | Note title can not be empty. 28 | Edit Body 29 | Edit Title 30 | Delete 31 | Filter 32 | Search 33 | 34 | Filter options 35 | ASC 36 | DESC 37 | Title 38 | Date 39 | Filter by title or date 40 | Ascending or descending order 41 | Apply 42 | Enter password 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/release/java/com/codingwithmitch/cleannotes/util/EspressoIdlingResource.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.util 2 | 3 | object EspressoIdlingResource { 4 | 5 | fun increment() { 6 | } 7 | 8 | fun decrement() { 9 | } 10 | 11 | fun clear(){ 12 | 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/test/java/com/codingwithmitch/cleannotes/business/data/NoteDataFactory.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.data 2 | 3 | import com.codingwithmitch.cleannotes.business.domain.model.Note 4 | import com.google.common.reflect.TypeToken 5 | import com.google.gson.Gson 6 | 7 | class NoteDataFactory( 8 | private val testClassLoader: ClassLoader 9 | ) { 10 | 11 | fun produceListOfNotes(): List{ 12 | val notes: List = Gson() 13 | .fromJson( 14 | getNotesFromFile("note_list.json"), 15 | object: TypeToken>() {}.type 16 | ) 17 | return notes 18 | } 19 | 20 | fun produceHashMapOfNotes(noteList: List): HashMap{ 21 | val map = HashMap() 22 | for(note in noteList){ 23 | map.put(note.id, note) 24 | } 25 | return map 26 | } 27 | 28 | fun produceEmptyListOfNotes(): List{ 29 | return ArrayList() 30 | } 31 | 32 | fun getNotesFromFile(fileName: String): String { 33 | return testClassLoader.getResource(fileName).readText() 34 | } 35 | 36 | } 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/test/java/com/codingwithmitch/cleannotes/business/data/network/FakeNoteNetworkDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.data.network 2 | 3 | import com.codingwithmitch.cleannotes.business.data.network.abstraction.NoteNetworkDataSource 4 | import com.codingwithmitch.cleannotes.business.domain.model.Note 5 | import com.codingwithmitch.cleannotes.business.domain.util.DateUtil 6 | 7 | class FakeNoteNetworkDataSourceImpl 8 | constructor( 9 | private val notesData: HashMap, 10 | private val deletedNotesData: HashMap, 11 | private val dateUtil: DateUtil 12 | ) : NoteNetworkDataSource{ 13 | 14 | override suspend fun insertOrUpdateNote(note: Note) { 15 | val n = Note( 16 | id = note.id, 17 | title = note.title, 18 | body = note.body, 19 | created_at = note.created_at, 20 | updated_at = dateUtil.getCurrentTimestamp() 21 | ) 22 | notesData.put(note.id, n) 23 | } 24 | 25 | override suspend fun deleteNote(primaryKey: String) { 26 | notesData.remove(primaryKey) 27 | } 28 | 29 | override suspend fun insertDeletedNote(note: Note) { 30 | deletedNotesData.put(note.id, note) 31 | } 32 | 33 | override suspend fun insertDeletedNotes(notes: List) { 34 | for(note in notes){ 35 | deletedNotesData.put(note.id, note) 36 | } 37 | } 38 | 39 | override suspend fun deleteDeletedNote(note: Note) { 40 | deletedNotesData.remove(note.id) 41 | } 42 | 43 | override suspend fun getDeletedNotes(): List { 44 | return ArrayList(deletedNotesData.values) 45 | } 46 | 47 | override suspend fun deleteAllNotes() { 48 | deletedNotesData.clear() 49 | } 50 | 51 | override suspend fun searchNote(note: Note): Note? { 52 | return notesData.get(note.id) 53 | } 54 | 55 | override suspend fun getAllNotes(): List { 56 | return ArrayList(notesData.values) 57 | } 58 | 59 | override suspend fun insertOrUpdateNotes(notes: List) { 60 | for(note in notes){ 61 | notesData.put(note.id, note) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/test/java/com/codingwithmitch/cleannotes/business/interactors/notelist/GetNumNotesTest.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.interactors.notelist 2 | 3 | import com.codingwithmitch.cleannotes.business.data.cache.abstraction.NoteCacheDataSource 4 | import com.codingwithmitch.cleannotes.business.domain.model.NoteFactory 5 | import com.codingwithmitch.cleannotes.business.interactors.notelist.GetNumNotes.Companion.GET_NUM_NOTES_SUCCESS 6 | import com.codingwithmitch.cleannotes.business.domain.state.DataState 7 | import com.codingwithmitch.cleannotes.di.DependencyContainer 8 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.state.NoteListStateEvent.* 9 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.state.NoteListViewState 10 | import kotlinx.coroutines.InternalCoroutinesApi 11 | import kotlinx.coroutines.flow.FlowCollector 12 | import kotlinx.coroutines.runBlocking 13 | import org.junit.jupiter.api.Assertions.* 14 | import org.junit.jupiter.api.Test 15 | 16 | /* 17 | 18 | Test cases: 19 | 1. getNumNotes_success_confirmCorrect() 20 | a) get the number of notes in cache 21 | b) listen for GET_NUM_NOTES_SUCCESS from flow emission 22 | c) compare with the number of notes in the fake data set 23 | 24 | */ 25 | @InternalCoroutinesApi 26 | class GetNumNotesTest { 27 | 28 | // system in test 29 | private val getNumNotes: GetNumNotes 30 | 31 | // dependencies 32 | private val dependencyContainer: DependencyContainer 33 | private val noteCacheDataSource: NoteCacheDataSource 34 | private val noteFactory: NoteFactory 35 | 36 | init { 37 | dependencyContainer = DependencyContainer() 38 | dependencyContainer.build() 39 | noteCacheDataSource = dependencyContainer.noteCacheDataSource 40 | noteFactory = dependencyContainer.noteFactory 41 | getNumNotes = GetNumNotes( 42 | noteCacheDataSource = noteCacheDataSource 43 | ) 44 | } 45 | 46 | 47 | @Test 48 | fun getNumNotes_success_confirmCorrect() = runBlocking { 49 | 50 | var numNotes = 0 51 | getNumNotes.getNumNotes( 52 | stateEvent = GetNumNotesInCacheEvent() 53 | ).collect(object: FlowCollector?>{ 54 | override suspend fun emit(value: DataState?) { 55 | assertEquals( 56 | value?.stateMessage?.response?.message, 57 | GET_NUM_NOTES_SUCCESS 58 | ) 59 | numNotes = value?.data?.numNotesInCache?: 0 60 | } 61 | }) 62 | 63 | val actualNumNotesInCache = noteCacheDataSource.getNumNotes() 64 | assertTrue { actualNumNotesInCache == numNotes } 65 | } 66 | 67 | 68 | } 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /app/src/test/java/com/codingwithmitch/cleannotes/business/interactors/splash/SyncDeletedNotesTest.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.business.interactors.splash 2 | 3 | import com.codingwithmitch.cleannotes.business.data.cache.abstraction.NoteCacheDataSource 4 | import com.codingwithmitch.cleannotes.business.data.network.abstraction.NoteNetworkDataSource 5 | import com.codingwithmitch.cleannotes.business.domain.model.Note 6 | import com.codingwithmitch.cleannotes.business.domain.model.NoteFactory 7 | import com.codingwithmitch.cleannotes.business.domain.util.DateUtil 8 | import com.codingwithmitch.cleannotes.di.DependencyContainer 9 | import kotlinx.coroutines.InternalCoroutinesApi 10 | import kotlinx.coroutines.runBlocking 11 | import org.junit.jupiter.api.Assertions.assertTrue 12 | import org.junit.jupiter.api.Test 13 | 14 | /* 15 | 16 | Test cases: 17 | 1. deleteNetworkNotes_confirmCacheSync() 18 | a) select some notes for deleting from network 19 | b) delete from network 20 | c) perform sync 21 | d) confirm notes from cache were deleted 22 | */ 23 | 24 | @InternalCoroutinesApi 25 | class SyncDeletedNotesTest { 26 | 27 | // system in test 28 | private val syncDeletedNotes: SyncDeletedNotes 29 | 30 | // dependencies 31 | private val dependencyContainer: DependencyContainer 32 | private val noteCacheDataSource: NoteCacheDataSource 33 | private val noteNetworkDataSource: NoteNetworkDataSource 34 | private val noteFactory: NoteFactory 35 | private val dateUtil: DateUtil 36 | 37 | init { 38 | dependencyContainer = DependencyContainer() 39 | dependencyContainer.build() 40 | noteCacheDataSource = dependencyContainer.noteCacheDataSource 41 | noteNetworkDataSource = dependencyContainer.noteNetworkDataSource 42 | noteFactory = dependencyContainer.noteFactory 43 | dateUtil = dependencyContainer.dateUtil 44 | syncDeletedNotes = SyncDeletedNotes( 45 | noteCacheDataSource = noteCacheDataSource, 46 | noteNetworkDataSource = noteNetworkDataSource 47 | ) 48 | } 49 | 50 | @Test 51 | fun deleteNetworkNotes_confirmCacheSync() = runBlocking { 52 | 53 | // select some notes to be deleted from cache 54 | val networkNotes = noteNetworkDataSource.getAllNotes() 55 | val notesToDelete: ArrayList = ArrayList() 56 | for(note in networkNotes){ 57 | notesToDelete.add(note) 58 | noteNetworkDataSource.deleteNote(note.id) 59 | noteNetworkDataSource.insertDeletedNote(note) 60 | if(notesToDelete.size > 3){ 61 | break 62 | } 63 | } 64 | 65 | // perform sync 66 | syncDeletedNotes.syncDeletedNotes() 67 | 68 | // confirm notes were deleted from cache 69 | for(note in notesToDelete){ 70 | val cachedNote = noteCacheDataSource.searchNoteById(note.id) 71 | assertTrue { cachedNote == null } 72 | } 73 | } 74 | } 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /app/src/test/java/com/codingwithmitch/cleannotes/di/DependencyContainer.kt: -------------------------------------------------------------------------------- 1 | package com.codingwithmitch.cleannotes.di 2 | 3 | import com.codingwithmitch.cleannotes.business.data.NoteDataFactory 4 | import com.codingwithmitch.cleannotes.business.data.cache.FakeNoteCacheDataSourceImpl 5 | import com.codingwithmitch.cleannotes.business.data.cache.abstraction.NoteCacheDataSource 6 | import com.codingwithmitch.cleannotes.business.data.network.FakeNoteNetworkDataSourceImpl 7 | import com.codingwithmitch.cleannotes.business.data.network.abstraction.NoteNetworkDataSource 8 | import com.codingwithmitch.cleannotes.business.domain.model.Note 9 | import com.codingwithmitch.cleannotes.business.domain.model.NoteFactory 10 | import com.codingwithmitch.cleannotes.business.domain.util.DateUtil 11 | import com.codingwithmitch.cleannotes.util.isUnitTest 12 | import java.text.SimpleDateFormat 13 | import java.util.* 14 | import kotlin.collections.HashMap 15 | 16 | class DependencyContainer { 17 | 18 | private val dateFormat = SimpleDateFormat("yyyy-MM-dd hh:mm:ss a", Locale.ENGLISH) 19 | val dateUtil = 20 | DateUtil(dateFormat) 21 | lateinit var noteNetworkDataSource: NoteNetworkDataSource 22 | lateinit var noteCacheDataSource: NoteCacheDataSource 23 | lateinit var noteFactory: NoteFactory 24 | lateinit var noteDataFactory: NoteDataFactory 25 | 26 | init { 27 | isUnitTest = true // for Logger.kt 28 | } 29 | 30 | fun build() { 31 | this.javaClass.classLoader?.let { classLoader -> 32 | noteDataFactory = NoteDataFactory(classLoader) 33 | } 34 | noteFactory = NoteFactory(dateUtil) 35 | noteNetworkDataSource = FakeNoteNetworkDataSourceImpl( 36 | notesData = noteDataFactory.produceHashMapOfNotes( 37 | noteDataFactory.produceListOfNotes() 38 | ), 39 | deletedNotesData = HashMap(), 40 | dateUtil = dateUtil 41 | ) 42 | noteCacheDataSource = FakeNoteCacheDataSourceImpl( 43 | notesData = noteDataFactory.produceHashMapOfNotes( 44 | noteDataFactory.produceListOfNotes() 45 | ), 46 | dateUtil = dateUtil 47 | ) 48 | } 49 | 50 | } 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /app/src/test/res/note_list.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "2474abea-7584-486b-9f88-87a21870b0ec", 4 | "title": "Vancouver PNE 2019", 5 | "body": "Here is Jess and I at the Vancouver PNE. We ate a lot of food.", 6 | "updated_at": "2019-04-14 08:41:22 AM", 7 | "created_at": "2019-04-14 07:05:11 AM" 8 | }, 9 | { 10 | "id": "2474fbaa-7884-4h6b-9b8z-87a21670b0ec", 11 | "title": "Ready for a Walk", 12 | "body": "Here I am at the park with my dogs Kiba and Maizy. Maizy is the smaller one and Kiba is the larger one.", 13 | "updated_at": "2019-04-17 11:05:24 PM", 14 | "created_at": "2019-04-15 04:44:57 AM" 15 | }, 16 | { 17 | "id": "2474mbaa-7884-4htb-9baz-87a216a0b0ec", 18 | "title": "Maizy Sleeping", 19 | "body": "I took this picture while Maizy was sleeping on the couch. She's very cute.", 20 | "updated_at": "2019-02-01 01:55:53 AM", 21 | "created_at": "2019-01-24 12:19:35 PM" 22 | }, 23 | { 24 | "id": "2474fpaa-k884-4u6b-9biz-87am1670b0ec", 25 | "title": "My Brother Blake", 26 | "body": "This is a picture of my brother Blake and I. We were taking some pictures for his website.", 27 | "updated_at": "2019-12-14 03:05:16 PM", 28 | "created_at": "2019-12-13 07:05:17 AM" 29 | }, 30 | { 31 | "id": "2474abaa-788a-4a6b-948z-87a2167hb0ec", 32 | "title": "Lounging Dogs", 33 | "body": "Kiba and Maizy are laying in the sun relaxing.", 34 | "updated_at": "2019-11-14 06:12:44 AM", 35 | "created_at": "2019-10-14 02:47:13 PM" 36 | }, 37 | { 38 | "id": "24742baa-78j4-4z6b-9b8l-87a11670b0ec", 39 | "title": "Mountains in Washington", 40 | "body": "This is an image I found somewhere on the internert. I love pictures like this. I believe it's in Washington, U.S.A.", 41 | "updated_at": "2019-05-19 11:34:16 PM", 42 | "created_at": "2019-04-25 05:16:36 AM" 43 | }, 44 | { 45 | "id": "2g74fbaa-78h4-4hab-9b85-87l21670b0ec", 46 | "title": "France Mountain Range", 47 | "body": "Another beautiful picture of nature. You can find more pictures like this one on Reddit.com, in the subreddit: '/r/earthporn'.", 48 | "updated_at": "2019-10-01 12:22:46 AM", 49 | "created_at": "2019-09-19 09:36:57 PM" 50 | }, 51 | { 52 | "id": "2477fbaa-7b84-4hjb-9bkl-87a2a670b0ec", 53 | "title": "Aldergrove Park", 54 | "body": "I walk Kiba and Maizy pretty much every day. Usually we go to a park in Aldergrove. It takes about 1 hour, 15 minutes to walk around the entire park.", 55 | "updated_at": "2019-06-12 12:58:55 AM", 56 | "created_at": "2019-03-19 08:49:41 PM" 57 | }, 58 | { 59 | "id": "2477fbbb-7b84-g5jb-9bkl-8741a670b0ec", 60 | "title": "My Computer", 61 | "body": "I sit on my computer all day.", 62 | "updated_at": "2019-03-11 12:58:55 AM", 63 | "created_at": "2019-01-15 11:49:41 PM" 64 | }, 65 | { 66 | "id": "247aaabb-7564-g5jb-9bkl-8741ah70b0ec", 67 | "title": "Courses", 68 | "body": "I make Android dev courses for a living.", 69 | "updated_at": "2019-05-04 09:44:55 AM", 70 | "created_at": "2019-01-15 10:11:41 PM" 71 | } 72 | ] -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | import dependencies.Versions 2 | import dependencies.Build 3 | 4 | 5 | buildscript { 6 | ext.kotlin_version = Versions.kotlin 7 | 8 | repositories { 9 | google() 10 | jcenter() 11 | } 12 | dependencies { 13 | classpath Build.build_tools 14 | classpath Build.kotlin_gradle_plugin 15 | classpath Build.google_services 16 | classpath Build.crashlytics_gradle 17 | } 18 | } 19 | 20 | allprojects { 21 | repositories { 22 | google() 23 | jcenter() 24 | 25 | } 26 | } 27 | 28 | task clean(type: Delete) { 29 | delete rootProject.buildDir 30 | } 31 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.kotlin.dsl.`kotlin-dsl` 2 | 3 | plugins { 4 | `kotlin-dsl` 5 | } 6 | repositories { 7 | jcenter() 8 | } -------------------------------------------------------------------------------- /buildSrc/build/classes/kotlin/main/META-INF/buildSrc.kotlin_module: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /buildSrc/build/classes/kotlin/main/dependencies/AnnotationProcessing.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/classes/kotlin/main/dependencies/AnnotationProcessing.class -------------------------------------------------------------------------------- /buildSrc/build/classes/kotlin/main/dependencies/Application.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/classes/kotlin/main/dependencies/Application.class -------------------------------------------------------------------------------- /buildSrc/build/classes/kotlin/main/dependencies/Build.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/classes/kotlin/main/dependencies/Build.class -------------------------------------------------------------------------------- /buildSrc/build/classes/kotlin/main/dependencies/Java.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/classes/kotlin/main/dependencies/Java.class -------------------------------------------------------------------------------- /buildSrc/build/classes/kotlin/main/dependencies/Repositories.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/classes/kotlin/main/dependencies/Repositories.class -------------------------------------------------------------------------------- /buildSrc/build/classes/kotlin/main/dependencies/Versions.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/classes/kotlin/main/dependencies/Versions.class -------------------------------------------------------------------------------- /buildSrc/build/classes/kotlin/main/dependencies/dependencies/AndroidTestDependencies.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/classes/kotlin/main/dependencies/dependencies/AndroidTestDependencies.class -------------------------------------------------------------------------------- /buildSrc/build/classes/kotlin/main/dependencies/dependencies/Dependencies.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/classes/kotlin/main/dependencies/dependencies/Dependencies.class -------------------------------------------------------------------------------- /buildSrc/build/classes/kotlin/main/dependencies/dependencies/SupportDependencies.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/classes/kotlin/main/dependencies/dependencies/SupportDependencies.class -------------------------------------------------------------------------------- /buildSrc/build/classes/kotlin/main/dependencies/dependencies/TestDependencies.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/classes/kotlin/main/dependencies/dependencies/TestDependencies.class -------------------------------------------------------------------------------- /buildSrc/build/kotlin/buildSrcjar-classes.txt: -------------------------------------------------------------------------------- 1 | D:\Android Studio Projects\CleanNotes\buildSrc\build\classes\kotlin\main\dependencies\AnnotationProcessing.class;D:\Android Studio Projects\CleanNotes\buildSrc\build\classes\kotlin\main\dependencies\Application.class;D:\Android Studio Projects\CleanNotes\buildSrc\build\classes\kotlin\main\dependencies\Build.class;D:\Android Studio Projects\CleanNotes\buildSrc\build\classes\kotlin\main\dependencies\Java.class;D:\Android Studio Projects\CleanNotes\buildSrc\build\classes\kotlin\main\dependencies\Repositories.class;D:\Android Studio Projects\CleanNotes\buildSrc\build\classes\kotlin\main\dependencies\Versions.class;D:\Android Studio Projects\CleanNotes\buildSrc\build\classes\kotlin\main\dependencies\dependencies\AndroidTestDependencies.class;D:\Android Studio Projects\CleanNotes\buildSrc\build\classes\kotlin\main\dependencies\dependencies\Dependencies.class;D:\Android Studio Projects\CleanNotes\buildSrc\build\classes\kotlin\main\dependencies\dependencies\SupportDependencies.class;D:\Android Studio Projects\CleanNotes\buildSrc\build\classes\kotlin\main\dependencies\dependencies\TestDependencies.class -------------------------------------------------------------------------------- /buildSrc/build/kotlin/compileKotlin/build-history.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/build-history.bin -------------------------------------------------------------------------------- /buildSrc/build/kotlin/compileKotlin/caches-jvm/inputs/source-to-output.tab: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/inputs/source-to-output.tab -------------------------------------------------------------------------------- /buildSrc/build/kotlin/compileKotlin/caches-jvm/inputs/source-to-output.tab_i: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/inputs/source-to-output.tab_i -------------------------------------------------------------------------------- /buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab -------------------------------------------------------------------------------- /buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i -------------------------------------------------------------------------------- /buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/internal-name-to-source.tab: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/internal-name-to-source.tab -------------------------------------------------------------------------------- /buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i -------------------------------------------------------------------------------- /buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/proto.tab: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/proto.tab -------------------------------------------------------------------------------- /buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/proto.tab_i: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/proto.tab_i -------------------------------------------------------------------------------- /buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/source-to-classes.tab: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/source-to-classes.tab -------------------------------------------------------------------------------- /buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/file-to-id.tab: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/file-to-id.tab -------------------------------------------------------------------------------- /buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/file-to-id.tab_i: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/file-to-id.tab_i -------------------------------------------------------------------------------- /buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/id-to-file.tab: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/id-to-file.tab -------------------------------------------------------------------------------- /buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/lookups.tab: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/lookups.tab -------------------------------------------------------------------------------- /buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/lookups.tab_i: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/lookups.tab_i -------------------------------------------------------------------------------- /buildSrc/build/kotlin/compileKotlin/last-build.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/last-build.bin -------------------------------------------------------------------------------- /buildSrc/build/libs/buildSrc.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/libs/buildSrc.jar -------------------------------------------------------------------------------- /buildSrc/build/pluginUnderTestMetadata/plugin-under-test-metadata.properties: -------------------------------------------------------------------------------- 1 | implementation-classpath=D\:/Android Studio Projects/CleanNotes/buildSrc/build/classes/java/main;D\:/Android Studio Projects/CleanNotes/buildSrc/build/classes/groovy/main;D\:/Android Studio Projects/CleanNotes/buildSrc/build/classes/kotlin/main;D\:/Android Studio Projects/CleanNotes/buildSrc/build/resources/main 2 | -------------------------------------------------------------------------------- /buildSrc/build/reports/task-properties/report.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/reports/task-properties/report.txt -------------------------------------------------------------------------------- /buildSrc/build/tmp/jar/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | 3 | -------------------------------------------------------------------------------- /buildSrc/src/main/java/AnnotationProcessing.kt: -------------------------------------------------------------------------------- 1 | package dependencies 2 | 3 | object AnnotationProcessing { 4 | val room_compiler = "androidx.room:room-compiler:${Versions.room}" 5 | val dagger_compiler = "com.google.dagger:dagger-compiler:${Versions.dagger}" 6 | val lifecycle_compiler = "androidx.lifecycle:lifecycle-compiler:${Versions.lifecycle_version}" 7 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/Application.kt: -------------------------------------------------------------------------------- 1 | package dependencies 2 | 3 | object Application { 4 | val id = "com.codingwithmitch.cleannotes" 5 | val version_code = 1 6 | val version_name = "1.0" 7 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/Build.kt: -------------------------------------------------------------------------------- 1 | package dependencies 2 | 3 | object Build { 4 | val build_tools = "com.android.tools.build:gradle:${Versions.gradle}" 5 | val kotlin_gradle_plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}" 6 | val google_services = "com.google.gms:google-services:${Versions.play_services}" 7 | val junit5 = "de.mannodermaus.gradle.plugins:android-junit5:1.3.2.0" 8 | val crashlytics_gradle = "com.google.firebase:firebase-crashlytics-gradle:${Versions.crashlytics_gradle}" 9 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/Java.kt: -------------------------------------------------------------------------------- 1 | package dependencies 2 | 3 | object Java { 4 | 5 | val java_version = "1.8" 6 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/Repositories.kt: -------------------------------------------------------------------------------- 1 | package dependencies 2 | 3 | object Repositories{ 4 | 5 | val fabric = "https://maven.fabric.io/public" 6 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/Versions.kt: -------------------------------------------------------------------------------- 1 | package dependencies 2 | 3 | object Versions { 4 | val gradle = "3.5.3" 5 | val compilesdk = 29 6 | val minsdk = 21 7 | val targetsdk = 29 8 | val kotlin = "1.3.61" 9 | val ktx = "1.2.0" 10 | val dagger = "2.25.4" 11 | val nav_components = "2.3.0-alpha06" 12 | val material_dialogs = "3.2.1" 13 | val room = "2.1.0" 14 | val appcompat = "1.1.0-rc01" 15 | val constraintlayout = "1.1.3" 16 | val material_design = "1.1.0" 17 | val play_core = "1.7.1" 18 | val play_services = "4.3.3" 19 | val leak_canary = "2.0-alpha-3" 20 | val swipe_refresh_layout = "1.1.0-alpha03" 21 | val firebase_firestore = "21.4.2" 22 | val firebase_analytics = "17.4.1" 23 | val firebase_crashlytics = "17.0.0" 24 | val firebase_auth = "19.3.0" 25 | val espresso_core = "3.1.1" 26 | val espresso_idling_resource = "3.2.0" 27 | val mockk_version = "1.9.2" 28 | val test_runner = "1.2.0" 29 | val test_core = "1.2.0" 30 | val coroutines_version = "1.3.0" 31 | val coroutines_play_services = "1.3.2" 32 | val lifecycle_version = "2.2.0-alpha03" 33 | val retrofit2_version = "2.6.0" 34 | val markdown_processor = "0.1.3" 35 | val junit_jupiter_version = "5.6.0" 36 | val junit_4_version = "4.12" 37 | val fragment_version = "1.2.0" 38 | val androidx_test_ext = "1.1.1" 39 | val crashlytics_gradle = "2.1.0" 40 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/dependencies/AndroidTestDependencies.kt: -------------------------------------------------------------------------------- 1 | package dependencies.dependencies 2 | 3 | import dependencies.Versions 4 | 5 | object AndroidTestDependencies{ 6 | 7 | val kotlin_test = "org.jetbrains.kotlin:kotlin-test-junit:${Versions.kotlin}" 8 | val coroutines_test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.coroutines_version}" 9 | val espresso_core = "androidx.test.espresso:espresso-core:${Versions.espresso_core}" 10 | val espresso_contrib = "androidx.test.espresso:espresso-contrib:${Versions.espresso_core}" 11 | val idling_resource = "androidx.test.espresso:espresso-idling-resource:${Versions.espresso_idling_resource}" 12 | val test_runner = "androidx.test:runner:${Versions.test_runner}" 13 | val test_rules = "androidx.test:rules:${Versions.test_runner}" 14 | val text_core_ktx = "androidx.test:core-ktx:${Versions.test_core}" 15 | val mockk_android = "io.mockk:mockk-android:${Versions.mockk_version}" 16 | val fragment_testing = "androidx.fragment:fragment-testing:${Versions.fragment_version}" 17 | val androidx_test_ext = "androidx.test.ext:junit-ktx:${Versions.androidx_test_ext}" 18 | val navigation_testing = "androidx.navigation:navigation-testing:${Versions.nav_components}" 19 | 20 | val instrumentation_runner = "com.codingwithmitch.cleannotes.framework.MockTestRunner" 21 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/dependencies/Dependencies.kt: -------------------------------------------------------------------------------- 1 | package dependencies.dependencies 2 | 3 | import dependencies.Versions 4 | 5 | object Dependencies { 6 | val kotlin_standard_library = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.kotlin}" 7 | val kotlin_reflect = "org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlin}" 8 | val ktx = "androidx.core:core-ktx:${Versions.ktx}" 9 | val kotlin_coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines_version}" 10 | val kotlin_coroutines_android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines_version}" 11 | val kotlin_coroutines_play_services = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:${Versions.coroutines_play_services}" 12 | val dagger = "com.google.dagger:dagger:${Versions.dagger}" 13 | val navigation_fragment = "androidx.navigation:navigation-fragment-ktx:${Versions.nav_components}" 14 | val navigation_runtime = "androidx.navigation:navigation-runtime:${Versions.nav_components}" 15 | val navigation_ui = "androidx.navigation:navigation-ui-ktx:${Versions.nav_components}" 16 | val navigation_dynamic = "androidx.navigation:navigation-dynamic-features-fragment:${Versions.nav_components}" 17 | val material_dialogs = "com.afollestad.material-dialogs:core:${Versions.material_dialogs}" 18 | val material_dialogs_input = "com.afollestad.material-dialogs:input:${Versions.material_dialogs}" 19 | val room_runtime = "androidx.room:room-runtime:${Versions.room}" 20 | val room_ktx = "androidx.room:room-ktx:${Versions.room}" 21 | val play_core = "com.google.android.play:core:${Versions.play_core}" 22 | val leak_canary = "com.squareup.leakcanary:leakcanary-android:${Versions.leak_canary}" 23 | val firebase_firestore = "com.google.firebase:firebase-firestore-ktx:${Versions.firebase_firestore}" 24 | val firebase_auth = "com.google.firebase:firebase-auth:${Versions.firebase_auth}" 25 | val firebase_analytics = "com.google.firebase:firebase-analytics-ktx:${Versions.firebase_analytics}" 26 | val firebase_crashlytics = "com.google.firebase:firebase-crashlytics:${Versions.firebase_crashlytics}" 27 | val lifecycle_runtime = "androidx.lifecycle:lifecycle-runtime:${Versions.lifecycle_version}" 28 | val lifecycle_coroutines = "androidx.lifecycle:lifecycle-livedata-ktx:${Versions.lifecycle_version}" 29 | val retrofit = "com.squareup.retrofit2:retrofit:${Versions.retrofit2_version}" 30 | val retrofit_gson = "com.squareup.retrofit2:converter-gson:${Versions.retrofit2_version}" 31 | val markdown_processor = "com.yydcdut:markdown-processor:${Versions.markdown_processor}" 32 | } 33 | -------------------------------------------------------------------------------- /buildSrc/src/main/java/dependencies/SupportDependencies.kt: -------------------------------------------------------------------------------- 1 | package dependencies.dependencies 2 | 3 | import dependencies.Versions 4 | 5 | object SupportDependencies { 6 | 7 | val appcompat = "androidx.appcompat:appcompat:${Versions.appcompat}" 8 | val constraintlayout = "androidx.constraintlayout:constraintlayout:${Versions.constraintlayout}" 9 | val material_design = "com.google.android.material:material:${Versions.material_design}" 10 | val swipe_refresh_layout = "androidx.swiperefreshlayout:swiperefreshlayout:${Versions.swipe_refresh_layout}" 11 | 12 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/dependencies/TestDependencies.kt: -------------------------------------------------------------------------------- 1 | package dependencies.dependencies 2 | 3 | import dependencies.Versions 4 | 5 | object TestDependencies { 6 | 7 | val jupiter_api = "org.junit.jupiter:junit-jupiter-api:${Versions.junit_jupiter_version}" 8 | val jupiter_params = "org.junit.jupiter:junit-jupiter-params:${Versions.junit_jupiter_version}" 9 | val jupiter_engine = "org.junit.jupiter:junit-jupiter-engine:${Versions.junit_jupiter_version}" 10 | val mockk = "io.mockk:mockk:${Versions.mockk_version}" 11 | val junit4 = "junit:junit:${Versions.junit_4_version}" 12 | } -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firestore": { 3 | "rules": "firestore.rules", 4 | "indexes": "firestore.indexes.json" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [], 3 | "fieldOverrides": [] 4 | } 5 | -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service cloud.firestore { 3 | match /databases/{database}/documents { 4 | match /{document=**} { 5 | allow read, write: if request.auth.uid != null; 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | #org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | org.gradle.parallel=true 23 | kapt.incremental.apt=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Mar 26 09:39:43 PDT 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | rootProject.name='Clean Notes' 3 | -------------------------------------------------------------------------------- /tests/firestore-debug.log: -------------------------------------------------------------------------------- 1 | API endpoint: http://localhost:8080 2 | If you are using a library that supports the FIRESTORE_EMULATOR_HOST environment variable, run: 3 | 4 | export FIRESTORE_EMULATOR_HOST=localhost:8080 5 | 6 | Dev App Server is now running. 7 | 8 | -------------------------------------------------------------------------------- /tests/run_tests.sh: -------------------------------------------------------------------------------- 1 | print_blue(){ 2 | printf "\e[1;34m$1\e[0m" 3 | } 4 | 5 | 6 | print_blue "\n\n\nStarting Firestore Local Emulator...\n" 7 | firebase emulators:exec --only functions,firestore "ui_and_unit_tests.sh" 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/ui_and_unit_tests.sh: -------------------------------------------------------------------------------- 1 | # Got these colors from a medium article (I can't remember author) 2 | # Functions for customizing colors(Optional) 3 | print_green(){ 4 | printf "\e[1;32m$1\e[0m" 5 | } 6 | print_blue(){ 7 | printf "\e[1;34m$1\e[0m" 8 | } 9 | 10 | #Start 11 | print_blue "\n\n\nStarting" 12 | 13 | print_blue "\n\n\ncd into working directory...\n" 14 | cd "/d/Android Studio Projects/CleanNotes/" 15 | 16 | print_blue "\n\n\nrun unit tests...\n" 17 | ./gradlew test 18 | print_green "\n\n\n unit tests COMPLETE.\n" 19 | 20 | print_blue "\n\n\n run androidTests...\n" 21 | ./gradlew connectedAndroidTest 22 | print_green "\n\n\n androidTests COMPLETE.\n" 23 | 24 | print_green "\n\n\n Done.\n" 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ui-debug.log: -------------------------------------------------------------------------------- 1 | Web / API server started at http://localhost:4000 2 | --------------------------------------------------------------------------------