Repository: dmilicic/android-clean-sample-app Branch: master Commit: 7dcc3aa037c7 Files: 111 Total size: 167.7 KB Directory structure: gitextract_jhfjo9q6/ ├── .gitignore ├── .idea/ │ ├── .name │ ├── codeStyleSettings.xml │ ├── compiler.xml │ ├── copyright/ │ │ └── profiles_settings.xml │ ├── gradle.xml │ ├── misc.xml │ ├── modules.xml │ ├── runConfigurations.xml │ └── vcs.xml ├── LICENSE ├── QA/ │ ├── findbugs/ │ │ └── findbugs-filter.xml │ └── quality.gradle ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── kodelabs/ │ │ └── mycosts/ │ │ └── ApplicationTest.java │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── kodelabs/ │ │ │ └── mycosts/ │ │ │ ├── AndroidApplication.java │ │ │ ├── domain/ │ │ │ │ ├── executor/ │ │ │ │ │ ├── Executor.java │ │ │ │ │ ├── MainThread.java │ │ │ │ │ └── impl/ │ │ │ │ │ └── ThreadExecutor.java │ │ │ │ ├── interactors/ │ │ │ │ │ ├── AddCostInteractor.java │ │ │ │ │ ├── DeleteCostInteractor.java │ │ │ │ │ ├── EditCostInteractor.java │ │ │ │ │ ├── GetAllCostsInteractor.java │ │ │ │ │ ├── GetCostByIdInteractor.java │ │ │ │ │ ├── base/ │ │ │ │ │ │ ├── AbstractInteractor.java │ │ │ │ │ │ └── Interactor.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── AddCostInteractorImpl.java │ │ │ │ │ ├── DeleteCostInteractorImpl.java │ │ │ │ │ ├── EditCostInteractorImpl.java │ │ │ │ │ ├── GetAllCostsInteractorImpl.java │ │ │ │ │ └── GetCostByIdInteractorImpl.java │ │ │ │ ├── model/ │ │ │ │ │ └── Cost.java │ │ │ │ └── repository/ │ │ │ │ └── CostRepository.java │ │ │ ├── network/ │ │ │ │ ├── RestClient.java │ │ │ │ ├── converters/ │ │ │ │ │ └── RESTModelConverter.java │ │ │ │ ├── model/ │ │ │ │ │ ├── Payload.java │ │ │ │ │ └── RESTCost.java │ │ │ │ └── services/ │ │ │ │ └── SyncService.java │ │ │ ├── presentation/ │ │ │ │ ├── animation/ │ │ │ │ │ └── AnimatorFactory.java │ │ │ │ ├── converter/ │ │ │ │ │ └── DailyTotalCostConverter.java │ │ │ │ ├── model/ │ │ │ │ │ └── DailyTotalCost.java │ │ │ │ ├── presenters/ │ │ │ │ │ ├── AbstractPresenter.java │ │ │ │ │ ├── AddCostPresenter.java │ │ │ │ │ ├── BasePresenter.java │ │ │ │ │ ├── EditCostPresenter.java │ │ │ │ │ ├── MainPresenter.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── AddCostPresenterImpl.java │ │ │ │ │ ├── EditCostPresenterImpl.java │ │ │ │ │ └── MainPresenterImpl.java │ │ │ │ └── ui/ │ │ │ │ ├── BaseView.java │ │ │ │ ├── activities/ │ │ │ │ │ ├── AboutActivity.java │ │ │ │ │ ├── AbstractCostActivity.java │ │ │ │ │ ├── AddCostActivity.java │ │ │ │ │ ├── EditCostActivity.java │ │ │ │ │ └── MainActivity.java │ │ │ │ ├── adapters/ │ │ │ │ │ └── CostItemAdapter.java │ │ │ │ ├── customviews/ │ │ │ │ │ ├── CostItemView.java │ │ │ │ │ └── ExpandedCostView.java │ │ │ │ ├── fragments/ │ │ │ │ │ └── DatePickerFragment.java │ │ │ │ └── listeners/ │ │ │ │ ├── IndividualCostViewClickListener.java │ │ │ │ └── RecyclerViewClickListener.java │ │ │ ├── storage/ │ │ │ │ ├── CostRepositoryImpl.java │ │ │ │ ├── contentprovider/ │ │ │ │ │ └── StubProvider.java │ │ │ │ ├── converters/ │ │ │ │ │ └── StorageModelConverter.java │ │ │ │ ├── database/ │ │ │ │ │ └── CostDatabase.java │ │ │ │ └── model/ │ │ │ │ └── Cost.java │ │ │ ├── sync/ │ │ │ │ ├── SyncAdapter.java │ │ │ │ ├── SyncService.java │ │ │ │ └── auth/ │ │ │ │ ├── Authenticator.java │ │ │ │ ├── AuthenticatorService.java │ │ │ │ └── DummyAccountProvider.java │ │ │ ├── threading/ │ │ │ │ └── MainThreadImpl.java │ │ │ └── utils/ │ │ │ ├── AuthUtils.java │ │ │ └── DateUtils.java │ │ └── res/ │ │ ├── anim/ │ │ │ └── hold.xml │ │ ├── drawable/ │ │ │ └── rounded_corner.xml │ │ ├── layout/ │ │ │ ├── activity_about.xml │ │ │ ├── activity_add_cost.xml │ │ │ ├── activity_main.xml │ │ │ ├── card_daily_cost_item.xml │ │ │ ├── card_expanded_daily_cost_item.xml │ │ │ ├── content_about.xml │ │ │ ├── content_add_cost.xml │ │ │ ├── expanded_cost_item.xml │ │ │ └── individual_cost_item.xml │ │ ├── menu/ │ │ │ ├── menu_add_cost.xml │ │ │ ├── menu_cost_item.xml │ │ │ └── menu_main.xml │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ ├── values-v21/ │ │ │ └── styles.xml │ │ ├── values-w820dp/ │ │ │ └── dimens.xml │ │ └── xml/ │ │ ├── authenticator.xml │ │ └── syncadapter.xml │ └── test/ │ └── java/ │ └── com/ │ └── kodelabs/ │ └── mycosts/ │ ├── domain/ │ │ └── interactors/ │ │ └── GetCostByIdTest.java │ ├── presentation/ │ │ └── converter/ │ │ └── DailyTotalCostConverterTest.java │ ├── threading/ │ │ └── TestMainThread.java │ └── util/ │ └── TestDateUtil.java ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea/workspace.xml /.idea/libraries .DS_Store /build /captures ======= # Built application files *.apk *.ap_ # Files for the Dalvik VM *.dex # Java class files *.class # Generated files bin/ gen/ # Gradle files .gradle/ build/ /*/build/ # Local configuration file (sdk path, etc) local.properties # Proguard folder generated by Eclipse proguard/ # Log Files *.log ================================================ FILE: .idea/.name ================================================ My Costs ================================================ FILE: .idea/codeStyleSettings.xml ================================================ ================================================ FILE: .idea/compiler.xml ================================================ ================================================ FILE: .idea/copyright/profiles_settings.xml ================================================ ================================================ FILE: .idea/gradle.xml ================================================ ================================================ FILE: .idea/misc.xml ================================================ Android SDK Android API 21 Platform ================================================ FILE: .idea/modules.xml ================================================ ================================================ FILE: .idea/runConfigurations.xml ================================================ ================================================ FILE: .idea/vcs.xml ================================================ ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 Dario Miličić Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: QA/findbugs/findbugs-filter.xml ================================================ ================================================ FILE: QA/quality.gradle ================================================ apply plugin: 'findbugs' task findbugs(type: FindBugs) { ignoreFailures = true effort = "default" reportLevel = "medium" excludeFilter = new File("${project.rootDir}/QA/findbugs/findbugs-filter.xml") classes = files("${project.rootDir}/app/build/intermediates/classes") source = fileTree('src/main/java/') classpath = files() reports { xml.enabled = false html.enabled = true html { destination "${project.buildDir}/reports/findbugs/findbugs-output.html" } } } ================================================ FILE: README.md ================================================ # Android Clean - Cost Tracker A sample cost-tracker app that showcases my Clean architecture approach to build Android applications. It is a simple app with **core features** that include: - Adding, editing and deleting a cost with a date, category, description and amount - Displaying a list of summarized costs day by day - Clicking on a summarized cost should display details of all costs for that day That's it. For now. You are free to download it, modify it, fork it and do anything you want with it. ## What is Clean Architecture? In Clean, code is separated into layers in an onion shape. The outer layers of the onion depend on the inner layers but the opposite is not true. It can have an arbitrary amount of layers but for most applications there are 3 layers: - Outer: Implementation layer - Middle: Presenter/Controller layer - Inner: Business logic layer The **implementation layer** is where everything framework specific happens. Framework specific code includes every line of code that is not solving your problem, this includes all Android stuff like creating activities and fragments, sending intents, and more general code like networking code and databases. The purpose of the **presenter/controller layer** is to act as a connector between your business logic and framework specific code. The most important layer is the **business logic layer**. This is where you actually solve the problem you want to solve building your app. This layer does not contain any framework specific code and you should be able to run it without an emulator. This way you can have your business logic code that is easy to test, develop and maintain. **That is the main benefit of Clean Architecture.** More general info about Clean Architecture can be found on this [blog]. This is a general explanation so let me explain how should it look like specifically in Android and how exactly do I build apps using Clean. ## How this app is structured I've found that the most practical way to build Android apps with Clean is with the following approach. This is how this sample app is structured: #### Outer layers - The **presentation** layer has a standard [MVP] structure. All Activites and Fragments, everything view related and user-facing is put into the layer. - Database specific code is inside the **storage** layer. - Network specific code is inside the **network** layer. - Any other framework specific code would be put into its own layer, for example in Android a **bluetooth** layer is something I often have. #### Inner/Core layer - Business logic is put into the **domain** layer. Although I am omitting a middle layer, that is not actually true. Because my presentation layer actually includes **Presenters**, this provides a good separation of code between presentation and domain layers. Communication between layers is done using interfaces as explained in the blog linked above. In short, the inner layer only uses an interface while its the job of the outer layer to implement it. This way the inner layer only cares about calling methods on an interface, without actually knowing what is going on under the hood. You can read more about it in my [detailed guide]. ### Syncing to backend There is a rails app I made that syncs all cost items to the server. You can find it here: https://mycosts-app.herokuapp.com/. This is a very simple app just to showcase the sync feature and its [source code] is open. Cost items are synced in real-time so you can see costs appearing on the website when you create them on Android. Editing and deleting are currently not supported. ## Future improvements This list of future improvements for this app and is something I may or may not actively work on. If anyone wants to get into contributing to open source, these can be a great way to start and are relatively easy to implement. I would be happy if it helps you learn and would accept a pull request if any of these are implemented: - There is only a list of daily costs, there should be a list for weekly, monthly and perhaps yearly. - A graph detailing how much a user spends on each category would be very useful. *Hint: Maybe use [MPAndroidChart]* - There is a lot of dependency injection going on, someone can reduce the amount of code by using a DI framework. *Hint: Dagger 2* - Nothing is displayed on the main screen when there are no costs. *This is the easiest one, we just need to display something to tell users there is nothing to display, doh.* - Add support for more than one currency. - There is a fixed amount of categories, someone could add a way to manually add more categories. - ??? License ---- MIT [//]: # (These are reference links used in the body of this note and get stripped out when the markdown processor does its job. There is no need to format nicely because it shouldn't be seen. Thanks SO - http://stackoverflow.com/questions/4823468/store-comments-in-markdown-syntax) [source code]: [detailed guide]: [blog]: [MVP]: [MPAndroidChart]: [DBFlow]: ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' apply plugin: 'com.neenbedankt.android-apt' apply from: "${project.rootDir}/QA/quality.gradle" android { compileSdkVersion 23 buildToolsVersion "23.0.2" defaultConfig { applicationId "com.kodelabs.mycosts" minSdkVersion 15 targetSdkVersion 23 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } def dbflow_version = "3.0.0-beta3" dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) // general compile 'com.android.support:appcompat-v7:23.1.1' compile 'com.android.support:design:23.1.1' compile 'com.jakewharton:butterknife:7.0.1' compile 'com.jakewharton.timber:timber:4.1.0' // inspection compile 'com.facebook.stetho:stetho:1.3.0' compile 'com.facebook.stetho:stetho-okhttp3:1.3.0' // network compile 'com.squareup.retrofit2:retrofit:2.0.0-beta4' compile 'com.squareup.retrofit2:converter-gson:+' compile 'com.squareup.okhttp3:logging-interceptor:3.0.1' // database apt "com.github.Raizlabs.DBFlow:dbflow-processor:${dbflow_version}" compile "com.github.Raizlabs.DBFlow:dbflow-core:${dbflow_version}" compile "com.github.Raizlabs.DBFlow:dbflow:${dbflow_version}" // material design compile 'com.android.support:cardview-v7:23.1.1' compile 'com.android.support:recyclerview-v7:23.1.1' compile('com.github.ozodrukh:CircularReveal:1.1.1@aar') { transitive = true; } // tests testCompile 'junit:junit:4.12' testCompile "org.mockito:mockito-core:1.+" } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Users/dmilicic/Documents/android-sdk-macosx/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: app/src/androidTest/java/com/kodelabs/mycosts/ApplicationTest.java ================================================ package com.kodelabs.mycosts; import android.app.Application; import android.test.ApplicationTestCase; /** * Testing Fundamentals */ public class ApplicationTest extends ApplicationTestCase { public ApplicationTest() { super(Application.class); } } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/AndroidApplication.java ================================================ package com.kodelabs.mycosts; import android.app.Application; import com.facebook.stetho.Stetho; import com.raizlabs.android.dbflow.config.FlowManager; import timber.log.Timber; import timber.log.Timber.DebugTree; /** * Created by dmilicic on 12/10/15. */ public class AndroidApplication extends Application { @Override public void onCreate() { super.onCreate(); // init database FlowManager.init(this); // enable logging Timber.plant(new DebugTree()); // enable stetho Stetho.initializeWithDefaults(this); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/domain/executor/Executor.java ================================================ package com.kodelabs.mycosts.domain.executor; import com.kodelabs.mycosts.domain.interactors.base.AbstractInteractor; /** * This executor is responsible for running interactors on background threads. *

* Created by dmilicic on 7/29/15. */ public interface Executor { /** * This method should call the interactor's run method and thus start the interactor. This should be called * on a background thread as interactors might do lengthy operations. * * @param interactor The interactor to run. */ void execute(final AbstractInteractor interactor); } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/domain/executor/MainThread.java ================================================ package com.kodelabs.mycosts.domain.executor; /** * This interface will define a class that will enable interactors to run certain operations on the main (UI) thread. For example, * if an interactor needs to show an object to the UI this can be used to make sure the show method is called on the UI * thread. *

* Created by dmilicic on 7/29/15. */ public interface MainThread { /** * Make runnable operation run in the main thread. * * @param runnable The runnable to run. */ void post(final Runnable runnable); } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/domain/executor/impl/ThreadExecutor.java ================================================ package com.kodelabs.mycosts.domain.executor.impl; import com.kodelabs.mycosts.domain.executor.Executor; import com.kodelabs.mycosts.domain.interactors.base.AbstractInteractor; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * This singleton class will make sure that each interactor operation gets a background thread. *

* Created by dmilicic on 7/29/15. */ public class ThreadExecutor implements Executor { // This is a singleton private static volatile ThreadExecutor sThreadExecutor; private static final int CORE_POOL_SIZE = 3; private static final int MAX_POOL_SIZE = 5; private static final int KEEP_ALIVE_TIME = 120; private static final TimeUnit TIME_UNIT = TimeUnit.SECONDS; private static final BlockingQueue WORK_QUEUE = new LinkedBlockingQueue(); private ThreadPoolExecutor mThreadPoolExecutor; private ThreadExecutor() { long keepAlive = KEEP_ALIVE_TIME; mThreadPoolExecutor = new ThreadPoolExecutor( CORE_POOL_SIZE, MAX_POOL_SIZE, keepAlive, TIME_UNIT, WORK_QUEUE); } @Override public void execute(final AbstractInteractor interactor) { mThreadPoolExecutor.submit(new Runnable() { @Override public void run() { // run the main logic interactor.run(); // mark it as finished interactor.onFinished(); } }); } /** * Returns a singleton instance of this executor. If the executor is not initialized then it initializes it and returns * the instance. */ public static Executor getInstance() { if (sThreadExecutor == null) { sThreadExecutor = new ThreadExecutor(); } return sThreadExecutor; } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/AddCostInteractor.java ================================================ package com.kodelabs.mycosts.domain.interactors; import com.kodelabs.mycosts.domain.interactors.base.Interactor; /** * Created by dmilicic on 12/23/15. */ public interface AddCostInteractor extends Interactor { interface Callback { void onCostAdded(); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/DeleteCostInteractor.java ================================================ package com.kodelabs.mycosts.domain.interactors; import com.kodelabs.mycosts.domain.interactors.base.Interactor; import com.kodelabs.mycosts.domain.model.Cost; /** * Created by dmilicic on 12/26/15. */ public interface DeleteCostInteractor extends Interactor { interface Callback { void onCostDeleted(Cost cost); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/EditCostInteractor.java ================================================ package com.kodelabs.mycosts.domain.interactors; import com.kodelabs.mycosts.domain.interactors.base.Interactor; import com.kodelabs.mycosts.domain.model.Cost; /** * Created by dmilicic on 12/26/15. */ public interface EditCostInteractor extends Interactor { interface Callback { void onCostUpdated(Cost cost); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/GetAllCostsInteractor.java ================================================ package com.kodelabs.mycosts.domain.interactors; import com.kodelabs.mycosts.domain.interactors.base.Interactor; import com.kodelabs.mycosts.domain.model.Cost; import java.util.List; /** * Created by dmilicic on 12/10/15. *

* This interactor is responsible for retrieving a list of costs from the database. */ public interface GetAllCostsInteractor extends Interactor { interface Callback { void onCostsRetrieved(List costList); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/GetCostByIdInteractor.java ================================================ package com.kodelabs.mycosts.domain.interactors; import com.kodelabs.mycosts.domain.interactors.base.Interactor; import com.kodelabs.mycosts.domain.model.Cost; /** * Created by dmilicic on 12/27/15. */ public interface GetCostByIdInteractor extends Interactor { interface Callback { void onCostRetrieved(Cost cost); void noCostFound(); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/base/AbstractInteractor.java ================================================ package com.kodelabs.mycosts.domain.interactors.base; import com.kodelabs.mycosts.domain.executor.Executor; import com.kodelabs.mycosts.domain.executor.MainThread; /** * Created by dmilicic on 8/4/15. *

* This abstract class implements some common methods for all interactors. Cancelling an interactor, check if its running * and finishing an interactor has mostly the same code throughout so that is why this class was created. Field methods * are declared volatile as we might use these methods from different threads (mainly from UI). *

* For example, when an activity is getting destroyed then we should probably cancel an interactor * but the request will come from the UI thread unless the request was specifically assigned to a background thread. */ public abstract class AbstractInteractor implements Interactor { protected Executor mThreadExecutor; protected MainThread mMainThread; protected volatile boolean mIsCanceled; protected volatile boolean mIsRunning; public AbstractInteractor(Executor threadExecutor, MainThread mainThread) { mThreadExecutor = threadExecutor; mMainThread = mainThread; } /** * This method contains the actual business logic of the interactor. It SHOULD NOT BE USED DIRECTLY but, instead, a * developer should call the execute() method of an interactor to make sure the operation is done on a background thread. *

* This method should only be called directly while doing unit/integration tests. That is the only reason it is declared * public as to help with easier testing. */ public abstract void run(); public void cancel() { mIsCanceled = true; mIsRunning = false; } public boolean isRunning() { return mIsRunning; } public void onFinished() { mIsRunning = false; mIsCanceled = false; } public void execute() { // mark this interactor as running this.mIsRunning = true; // start running this interactor in a background thread mThreadExecutor.execute(this); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/base/Interactor.java ================================================ package com.kodelabs.mycosts.domain.interactors.base; /** * Created by dmilicic on 12/13/15. */ public interface Interactor { /** * This is the main method that starts an interactor. It will make sure that the interactor operation is done on a * background thread. */ void execute(); } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/impl/AddCostInteractorImpl.java ================================================ package com.kodelabs.mycosts.domain.interactors.impl; import com.kodelabs.mycosts.domain.executor.Executor; import com.kodelabs.mycosts.domain.executor.MainThread; import com.kodelabs.mycosts.domain.interactors.AddCostInteractor; import com.kodelabs.mycosts.domain.interactors.base.AbstractInteractor; import com.kodelabs.mycosts.domain.model.Cost; import com.kodelabs.mycosts.domain.repository.CostRepository; import java.util.Date; /** * This interactor is responsible for creating and adding a new cost item into the database. It should get all the data needed to create * a new cost object and it should insert it in our repository. *

* Created by dmilicic on 12/23/15. */ public class AddCostInteractorImpl extends AbstractInteractor implements AddCostInteractor { private AddCostInteractor.Callback mCallback; private CostRepository mCostRepository; private String mCategory; private String mDescription; private Date mDate; private double mAmount; public AddCostInteractorImpl(Executor threadExecutor, MainThread mainThread, Callback callback, CostRepository costRepository, String category, String description, Date date, double amount) { super(threadExecutor, mainThread); mCallback = callback; mCostRepository = costRepository; mCategory = category; mDescription = description; mDate = date; mAmount = amount; } @Override public void run() { // create a new cost object and insert it Cost cost = new Cost(mCategory, mDescription, mDate, mAmount); mCostRepository.insert(cost); // notify on the main thread that we have inserted this item mMainThread.post(new Runnable() { @Override public void run() { mCallback.onCostAdded(); } }); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/impl/DeleteCostInteractorImpl.java ================================================ package com.kodelabs.mycosts.domain.interactors.impl; import com.kodelabs.mycosts.domain.executor.Executor; import com.kodelabs.mycosts.domain.executor.MainThread; import com.kodelabs.mycosts.domain.interactors.DeleteCostInteractor; import com.kodelabs.mycosts.domain.interactors.base.AbstractInteractor; import com.kodelabs.mycosts.domain.model.Cost; import com.kodelabs.mycosts.domain.repository.CostRepository; /** * Interactor responsible for deleting a cost from the database. *

* Created by dmilicic on 12/26/15. */ public class DeleteCostInteractorImpl extends AbstractInteractor implements DeleteCostInteractor { private long mCostId; private DeleteCostInteractor.Callback mCallback; private CostRepository mCostRepository; public DeleteCostInteractorImpl(Executor threadExecutor, MainThread mainThread, long costId, Callback callback, CostRepository costRepository) { super(threadExecutor, mainThread); mCostId = costId; mCallback = callback; mCostRepository = costRepository; } @Override public void run() { // check if this object even exists final Cost cost = mCostRepository.getCostById(mCostId); // delete this cost item if (cost != null) mCostRepository.delete(cost); // notify on the main thread mMainThread.post(new Runnable() { @Override public void run() { mCallback.onCostDeleted(cost); } }); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/impl/EditCostInteractorImpl.java ================================================ package com.kodelabs.mycosts.domain.interactors.impl; import com.kodelabs.mycosts.domain.executor.Executor; import com.kodelabs.mycosts.domain.executor.MainThread; import com.kodelabs.mycosts.domain.interactors.EditCostInteractor; import com.kodelabs.mycosts.domain.interactors.base.AbstractInteractor; import com.kodelabs.mycosts.domain.model.Cost; import com.kodelabs.mycosts.domain.repository.CostRepository; import java.util.Date; /** * This interactor handles editing a cost item. It should update it if it exists in the database, otherwise it should insert it. *

* Created by dmilicic on 12/26/15. */ public class EditCostInteractorImpl extends AbstractInteractor implements EditCostInteractor { private EditCostInteractor.Callback mCallback; private CostRepository mCostRepository; private Cost mUpdatedCost; private String mCategory; private String mDescription; private Date mDate; private double mAmount; public EditCostInteractorImpl(Executor threadExecutor, MainThread mainThread, Callback callback, CostRepository costRepository, Cost updatedCost, String category, String description, Date date, double amount) { super(threadExecutor, mainThread); mCallback = callback; mCostRepository = costRepository; mUpdatedCost = updatedCost; mCategory = category; mDescription = description; mDate = date; mAmount = amount; } @Override public void run() { // check if it exists in the database long costId = mUpdatedCost.getId(); Cost costToEdit = mCostRepository.getCostById(costId); // there is no item with this ID in the database, lets insert it if (costToEdit == null) { costToEdit = new Cost(mCategory, mDescription, mDate, mAmount); mCostRepository.insert(costToEdit); } else { // we found the item in the database, lets update it // update the cost costToEdit.setAmount(mAmount); costToEdit.setCategory(mCategory); costToEdit.setDate(mDate); costToEdit.setDescription(mDescription); // update in the db mCostRepository.update(costToEdit); } // notify on main thread that the update was successful mMainThread.post(new Runnable() { @Override public void run() { mCallback.onCostUpdated(mUpdatedCost); } }); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/impl/GetAllCostsInteractorImpl.java ================================================ package com.kodelabs.mycosts.domain.interactors.impl; import com.kodelabs.mycosts.domain.executor.Executor; import com.kodelabs.mycosts.domain.executor.MainThread; import com.kodelabs.mycosts.domain.interactors.GetAllCostsInteractor; import com.kodelabs.mycosts.domain.interactors.base.AbstractInteractor; import com.kodelabs.mycosts.domain.model.Cost; import com.kodelabs.mycosts.domain.repository.CostRepository; import java.util.Collections; import java.util.Comparator; import java.util.List; /** * This interactor handles getting all costs from the database in a sorted manner. Costs should be sorted by date with * the most recent one coming first and the oldest one coming last. *

* Created by dmilicic on 12/10/15. */ public class GetAllCostsInteractorImpl extends AbstractInteractor implements GetAllCostsInteractor { private Callback mCallback; private CostRepository mCostRepository; private Comparator mCostComparator = new Comparator() { @Override public int compare(Cost lhs, Cost rhs) { if (lhs.getDate().before(rhs.getDate())) return 1; if (rhs.getDate().before(lhs.getDate())) return -1; return 0; } }; public GetAllCostsInteractorImpl(Executor threadExecutor, MainThread mainThread, CostRepository costRepository, Callback callback) { super(threadExecutor, mainThread); if (costRepository == null || callback == null) { throw new IllegalArgumentException("Arguments can not be null!"); } mCostRepository = costRepository; mCallback = callback; } @Override public void run() { // retrieve the costs from the database final List costs = mCostRepository.getAllCosts(); // sort them so the most recent cost items come first, and oldest comes last Collections.sort(costs, mCostComparator); // Show costs on the main thread mMainThread.post(new Runnable() { @Override public void run() { mCallback.onCostsRetrieved(costs); } }); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/impl/GetCostByIdInteractorImpl.java ================================================ package com.kodelabs.mycosts.domain.interactors.impl; import com.kodelabs.mycosts.domain.executor.Executor; import com.kodelabs.mycosts.domain.executor.MainThread; import com.kodelabs.mycosts.domain.interactors.GetCostByIdInteractor; import com.kodelabs.mycosts.domain.interactors.base.AbstractInteractor; import com.kodelabs.mycosts.domain.model.Cost; import com.kodelabs.mycosts.domain.repository.CostRepository; /** * Interactor responsible for getting a single cost item from the database using its ID. It should return the cost item * or notify if there isn't one. *

* Created by dmilicic on 12/27/15. */ public class GetCostByIdInteractorImpl extends AbstractInteractor implements GetCostByIdInteractor { private long mCostId; private CostRepository mCostRepository; private GetCostByIdInteractor.Callback mCallback; public GetCostByIdInteractorImpl(Executor threadExecutor, MainThread mainThread, long costId, CostRepository costRepository, Callback callback) { super(threadExecutor, mainThread); mCostId = costId; mCostRepository = costRepository; mCallback = callback; } @Override public void run() { final Cost cost = mCostRepository.getCostById(mCostId); if (cost == null) { // we didn't find the cost we were looking for // notify this on the main thread mMainThread.post(new Runnable() { @Override public void run() { mCallback.noCostFound(); } }); } else { // we found it! // send it on the main thread mMainThread.post(new Runnable() { @Override public void run() { mCallback.onCostRetrieved(cost); } }); } } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/domain/model/Cost.java ================================================ package com.kodelabs.mycosts.domain.model; import java.util.Date; /** * Created by dmilicic on 12/10/15. */ public class Cost { private long mId; private String mCategory; private String mDescription; private Date mDate; private double mAmount; public Cost(String category, String description, Date date, double amount) { // cost will be "uniquely" identified by the current timestamp mId = new Date().getTime(); mCategory = category; mDescription = description; mDate = date; mAmount = amount; } /** * This constructor should be used when we are converting an already existing cost item to this POJO, so we already have * an id variable. */ public Cost(String category, String description, Date date, double amount, long id) { mId = id; mCategory = category; mDescription = description; mDate = date; mAmount = amount; } public void setCategory(String category) { mCategory = category; } public void setDescription(String description) { mDescription = description; } public void setDate(Date date) { mDate = date; } public void setAmount(double amount) { mAmount = amount; } public long getId() { return mId; } public String getCategory() { return mCategory; } public String getDescription() { return mDescription; } public Date getDate() { return mDate; } public double getAmount() { return mAmount; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Cost cost = (Cost) o; return mId == cost.mId; } @Override public int hashCode() { return (int) (mId ^ (mId >>> 32)); } @Override public String toString() { return "Cost{" + "mId=" + mId + ", mCategory='" + mCategory + '\'' + ", mDescription='" + mDescription + '\'' + ", mDate=" + mDate + ", mAmount=" + mAmount + '}'; } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/domain/repository/CostRepository.java ================================================ package com.kodelabs.mycosts.domain.repository; import com.kodelabs.mycosts.domain.model.Cost; import java.util.List; /** * Created by dmilicic on 12/13/15. */ public interface CostRepository { void insert(Cost cost); void update(Cost cost); Cost getCostById(long id); List getAllCosts(); List getAllUnsyncedCosts(); void markSynced(List costs); void delete(Cost cost); } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/network/RestClient.java ================================================ package com.kodelabs.mycosts.network; import com.facebook.stetho.okhttp3.StethoInterceptor; import okhttp3.OkHttpClient; import okhttp3.logging.HttpLoggingInterceptor; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; /** *

* This is the main entry point for network communication. Use this class for instancing REST services which do the * actual communication. */ public class RestClient { /** * This is our main backend/server URL. */ public static final String REST_API_URL = "https://mycosts-app.herokuapp.com/"; // public static final String REST_API_URL = "http://192.168.0.12:3000"; private static Retrofit s_retrofit; static { // enable logging HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(); interceptor.setLevel(HttpLoggingInterceptor.Level.BODY); OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(interceptor) .addNetworkInterceptor(new StethoInterceptor()) .build(); s_retrofit = new Retrofit.Builder() .baseUrl(REST_API_URL) .addConverterFactory(GsonConverterFactory.create()) .client(client) .build(); } public static T getService(Class serviceClass) { return s_retrofit.create(serviceClass); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/network/converters/RESTModelConverter.java ================================================ package com.kodelabs.mycosts.network.converters; import com.kodelabs.mycosts.domain.model.Cost; import com.kodelabs.mycosts.network.model.RESTCost; import java.util.Date; /** * Created by dmilicic on 2/14/16. */ public class RESTModelConverter { public static RESTCost convertToRestModel(Cost cost) { String desc = cost.getDescription(); double amount = cost.getAmount(); String category = cost.getCategory(); Date date = cost.getDate(); long id = cost.getId(); return new RESTCost(id, category, desc, date, amount); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/network/model/Payload.java ================================================ package com.kodelabs.mycosts.network.model; import com.google.gson.Gson; import com.google.gson.annotations.SerializedName; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * Created by dmilicic on 2/14/16. */ public class Payload { @SerializedName("user") private String mUsername; @SerializedName("costs") private List mCosts; public Payload(String username) { mUsername = username; mCosts = new ArrayList<>(); } public String getUsername() { return mUsername; } public List getCosts() { return mCosts; } public void addCost(RESTCost cost) { mCosts.add(cost); } public static void main(String[] args) { Gson gson = new Gson(); RESTCost cost = new RESTCost(100, "category", "desc", new Date(), 100.0); String username = "user"; Payload data = new Payload(username); data.addCost(cost); cost = new RESTCost(200, "category", "desc", new Date(), 100.0); data.addCost(cost); cost = new RESTCost(300, "category", "desc", new Date(), 100.0); data.addCost(cost); String json = gson.toJson(data); System.out.println(json); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/network/model/RESTCost.java ================================================ package com.kodelabs.mycosts.network.model; import com.google.gson.annotations.SerializedName; import java.util.Date; /** * Created by dmilicic on 2/14/16. */ public class RESTCost { @SerializedName("id") private long mId; @SerializedName("category") private String mCategory; @SerializedName("description") private String mDescription; @SerializedName("date") private Date mDate; @SerializedName("amount") private double mAmount; public RESTCost(long id, String category, String description, Date date, double amount) { mId = id; mCategory = category; mDescription = description; mDate = date; mAmount = amount; } public long getId() { return mId; } public String getCategory() { return mCategory; } public String getDescription() { return mDescription; } public Date getDate() { return mDate; } public double getAmount() { return mAmount; } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/network/services/SyncService.java ================================================ package com.kodelabs.mycosts.network.services; import com.kodelabs.mycosts.network.model.Payload; import retrofit2.Call; import retrofit2.http.Body; import retrofit2.http.Headers; import retrofit2.http.POST; /** * A REST interface describing how data will be synced with the backend. *

*/ public interface SyncService { /** * This endpoint will be used to send new costs created on this device. */ @Headers("Connection: close") @POST("/costs") Call uploadData(@Body Payload data); } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/presentation/animation/AnimatorFactory.java ================================================ package com.kodelabs.mycosts.presentation.animation; import android.animation.Animator; import android.animation.Animator.AnimatorListener; import android.app.Activity; import android.content.Intent; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.view.View; import android.view.ViewAnimationUtils; import android.view.ViewGroup; import com.kodelabs.mycosts.R; import io.codetail.animation.SupportAnimator; /** * Created by dmilicic on 1/7/16. */ public class AnimatorFactory { public static final int REVEAL_ANIMATION_LENGTH = 350; // in milliseconds /** * Creates a circural reveal animation from a given source view. While revealing it uses the reveal layout and after * the animation completes, it starts the activity given in the intent. * * @param src The source view from which the circular animation starts. * @param revealLayout The layout to reveal in the animation. * @param intent The intent used to start another activity. * @param activity The activity is needed as a context object. */ public static void enterReveal(ViewGroup revealLayout, final Intent intent, final Activity activity) { int cx = (revealLayout.getLeft() + revealLayout.getRight()); int cy = revealLayout.getTop(); int finalRadius = Math.max(revealLayout.getWidth(), revealLayout.getHeight()); AnimatorListener animatorListener = new AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { activity.startActivity(intent); activity.overridePendingTransition(0, R.anim.hold); } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }; // src.setVisibility(View.INVISIBLE); // make the view visible and start the animation revealLayout.setVisibility(View.VISIBLE); if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { Animator anim = ViewAnimationUtils.createCircularReveal(revealLayout, cx, cy, 0, finalRadius); anim.setDuration(REVEAL_ANIMATION_LENGTH); anim.addListener(animatorListener); anim.start(); } else { // create the animator for this view (the start radius is zero) SupportAnimator anim = io.codetail.animation.ViewAnimationUtils.createCircularReveal(revealLayout, cx, cy, 0, finalRadius); anim.setDuration(REVEAL_ANIMATION_LENGTH); anim.addListener(new SupportAnimator.AnimatorListener() { @Override public void onAnimationStart() { } @Override public void onAnimationEnd() { activity.startActivity(intent); activity.overridePendingTransition(0, R.anim.hold); } @Override public void onAnimationCancel() { } @Override public void onAnimationRepeat() { } }); anim.start(); } } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/presentation/converter/DailyTotalCostConverter.java ================================================ package com.kodelabs.mycosts.presentation.converter; import com.kodelabs.mycosts.domain.model.Cost; import com.kodelabs.mycosts.presentation.model.DailyTotalCost; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * Created by dmilicic on 1/4/16. */ public class DailyTotalCostConverter { public static List convertCostsToDailyCosts(List costList) { List result = new ArrayList<>(); if (costList == null || costList.size() == 0) { // return an empty array if we have nothing to convert return result; } // declare and initialize data vars List dailyCosts = new ArrayList<>(); Date currentDate = costList.get(0).getDate(); Cost cost; // iterate over all cost items received for (int idx = 0; idx < costList.size(); idx++) { cost = costList.get(idx); if (idx == 0) { // in case this is the first element // initialize the process dailyCosts = new ArrayList<>(); currentDate = cost.getDate(); } // add the current element to the list of daily costs - for the current date dailyCosts.add(cost); // check if this is the last element if (idx == costList.size() - 1) { // create a new daily total match DailyTotalCost dailyTotalCost = new DailyTotalCost(dailyCosts, currentDate); result.add(dailyTotalCost); continue; } // get the next element Cost nextCost = costList.get(idx + 1); // check if the next element is from a different day if (!nextCost.getDate().equals(currentDate)) { // create a new daily total match DailyTotalCost dailyTotalCost = new DailyTotalCost(dailyCosts, currentDate); result.add(dailyTotalCost); // repeat the process with the next item dailyCosts = new ArrayList<>(); currentDate = nextCost.getDate(); } } return result; } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/presentation/model/DailyTotalCost.java ================================================ package com.kodelabs.mycosts.presentation.model; import com.kodelabs.mycosts.domain.model.Cost; import java.util.Date; import java.util.List; /** * Created by dmilicic on 1/4/16. */ public class DailyTotalCost { private List mCostList; private Date mDate; private double mTotalCost; public DailyTotalCost(List costList, Date date) { mCostList = costList; mDate = date; // eagerly calculate the total cost mTotalCost = 0.0; for (int idx = 0; idx < costList.size(); idx++) { mTotalCost += costList.get(idx).getAmount(); } } public List getCostList() { return mCostList; } public Date getDate() { return mDate; } public double getTotalCost() { return mTotalCost; } @Override public String toString() { return "DailyTotalCost{" + "mCostList=" + mCostList + ", mDate=" + mDate + ", mTotalCost=" + mTotalCost + '}'; } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/presentation/presenters/AbstractPresenter.java ================================================ package com.kodelabs.mycosts.presentation.presenters; import com.kodelabs.mycosts.domain.executor.Executor; import com.kodelabs.mycosts.domain.executor.MainThread; /** * Created by dmilicic on 12/23/15. */ public abstract class AbstractPresenter { protected Executor mExecutor; protected MainThread mMainThread; public AbstractPresenter(Executor executor, MainThread mainThread) { mExecutor = executor; mMainThread = mainThread; } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/presentation/presenters/AddCostPresenter.java ================================================ package com.kodelabs.mycosts.presentation.presenters; import com.kodelabs.mycosts.presentation.ui.BaseView; import java.util.Date; /** * Created by dmilicic on 12/21/15. */ public interface AddCostPresenter extends BasePresenter { interface View extends BaseView { void onCostAdded(); } void addNewCost(Date date, double amount, String description, String category); } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/presentation/presenters/BasePresenter.java ================================================ package com.kodelabs.mycosts.presentation.presenters; /** * Created by dmilicic on 7/28/15. */ public interface BasePresenter { /** * Method that control the lifecycle of the view. It should be called in the view's * (Activity or Fragment) onResume() method. */ void resume(); /** * Method that controls the lifecycle of the view. It should be called in the view's * (Activity or Fragment) onPause() method. */ void pause(); /** * Method that controls the lifecycle of the view. It should be called in the view's * (Activity or Fragment) onStop() method. */ void stop(); /** * Method that control the lifecycle of the view. It should be called in the view's * (Activity or Fragment) onDestroy() method. */ void destroy(); /** * Method that should signal the appropriate view to show the appropriate error with the provided message. */ void onError(String message); } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/presentation/presenters/EditCostPresenter.java ================================================ package com.kodelabs.mycosts.presentation.presenters; import com.kodelabs.mycosts.domain.model.Cost; import com.kodelabs.mycosts.presentation.ui.BaseView; import java.util.Date; /** * Created by dmilicic on 12/27/15. */ public interface EditCostPresenter { interface View extends BaseView { void onCostRetrieved(Cost cost); void onCostUpdated(Cost cost); } void getCostById(long id); void editCost(Cost cost, Date date, double amount, String description, String category); } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/presentation/presenters/MainPresenter.java ================================================ package com.kodelabs.mycosts.presentation.presenters; import com.kodelabs.mycosts.domain.model.Cost; import com.kodelabs.mycosts.presentation.model.DailyTotalCost; import com.kodelabs.mycosts.presentation.ui.BaseView; import java.util.List; /** * Created by dmilicic on 12/10/15. */ public interface MainPresenter extends BasePresenter { interface View extends BaseView { void showCosts(List costs); void onClickDeleteCost(long costId); void onClickEditCost(long costId, int position); void onCostDeleted(Cost cost); } void getAllCosts(); void deleteCost(long costId); } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/presentation/presenters/impl/AddCostPresenterImpl.java ================================================ package com.kodelabs.mycosts.presentation.presenters.impl; import com.kodelabs.mycosts.domain.executor.Executor; import com.kodelabs.mycosts.domain.executor.MainThread; import com.kodelabs.mycosts.domain.interactors.AddCostInteractor; import com.kodelabs.mycosts.domain.interactors.impl.AddCostInteractorImpl; import com.kodelabs.mycosts.domain.repository.CostRepository; import com.kodelabs.mycosts.presentation.presenters.AbstractPresenter; import com.kodelabs.mycosts.presentation.presenters.AddCostPresenter; import java.util.Date; /** * Created by dmilicic on 12/23/15. */ public class AddCostPresenterImpl extends AbstractPresenter implements AddCostPresenter, AddCostInteractor.Callback { private AddCostPresenter.View mView; private CostRepository mCostRepository; public AddCostPresenterImpl(Executor executor, MainThread mainThread, View view, CostRepository costRepository) { super(executor, mainThread); mView = view; mCostRepository = costRepository; } @Override public void addNewCost(Date date, double amount, String description, String category) { AddCostInteractor addCostInteractor = new AddCostInteractorImpl(mExecutor, mMainThread, this, mCostRepository, category, description, date, amount); addCostInteractor.execute(); } @Override public void onCostAdded() { mView.onCostAdded(); } @Override public void resume() { } @Override public void pause() { } @Override public void stop() { } @Override public void destroy() { } @Override public void onError(String message) { } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/presentation/presenters/impl/EditCostPresenterImpl.java ================================================ package com.kodelabs.mycosts.presentation.presenters.impl; import com.kodelabs.mycosts.domain.executor.Executor; import com.kodelabs.mycosts.domain.executor.MainThread; import com.kodelabs.mycosts.domain.interactors.EditCostInteractor; import com.kodelabs.mycosts.domain.interactors.GetCostByIdInteractor; import com.kodelabs.mycosts.domain.interactors.impl.EditCostInteractorImpl; import com.kodelabs.mycosts.domain.interactors.impl.GetCostByIdInteractorImpl; import com.kodelabs.mycosts.domain.model.Cost; import com.kodelabs.mycosts.domain.repository.CostRepository; import com.kodelabs.mycosts.presentation.presenters.AbstractPresenter; import com.kodelabs.mycosts.presentation.presenters.EditCostPresenter; import java.util.Date; /** * Created by dmilicic on 12/27/15. */ public class EditCostPresenterImpl extends AbstractPresenter implements EditCostPresenter, GetCostByIdInteractor.Callback, EditCostInteractor.Callback { private EditCostPresenter.View mView; private CostRepository mCostRepository; public EditCostPresenterImpl(Executor executor, MainThread mainThread, View view, CostRepository costRepository) { super(executor, mainThread); mView = view; mCostRepository = costRepository; } @Override public void getCostById(long id) { GetCostByIdInteractor getCostByIdInteractor = new GetCostByIdInteractorImpl( mExecutor, mMainThread, id, mCostRepository, this ); getCostByIdInteractor.execute(); } @Override public void onCostRetrieved(Cost cost) { mView.onCostRetrieved(cost); } @Override public void noCostFound() { mView.showError("No cost found :("); } @Override public void editCost(Cost cost, Date date, double amount, String description, String category) { EditCostInteractor editCostInteractor = new EditCostInteractorImpl( mExecutor, mMainThread, this, mCostRepository, cost, category, description, date, amount ); editCostInteractor.execute(); } @Override public void onCostUpdated(Cost cost) { mView.onCostUpdated(cost); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/presentation/presenters/impl/MainPresenterImpl.java ================================================ package com.kodelabs.mycosts.presentation.presenters.impl; import com.kodelabs.mycosts.domain.executor.Executor; import com.kodelabs.mycosts.domain.executor.MainThread; import com.kodelabs.mycosts.domain.interactors.DeleteCostInteractor; import com.kodelabs.mycosts.domain.interactors.GetAllCostsInteractor; import com.kodelabs.mycosts.domain.interactors.impl.DeleteCostInteractorImpl; import com.kodelabs.mycosts.domain.interactors.impl.GetAllCostsInteractorImpl; import com.kodelabs.mycosts.domain.model.Cost; import com.kodelabs.mycosts.domain.repository.CostRepository; import com.kodelabs.mycosts.presentation.converter.DailyTotalCostConverter; import com.kodelabs.mycosts.presentation.model.DailyTotalCost; import com.kodelabs.mycosts.presentation.presenters.AbstractPresenter; import com.kodelabs.mycosts.presentation.presenters.MainPresenter; import java.util.List; /** * Created by dmilicic on 12/13/15. */ public class MainPresenterImpl extends AbstractPresenter implements MainPresenter, GetAllCostsInteractor.Callback, DeleteCostInteractor.Callback { private MainPresenter.View mView; private CostRepository mCostRepository; public MainPresenterImpl(Executor executor, MainThread mainThread, View view, CostRepository costRepository) { super(executor, mainThread); mView = view; mCostRepository = costRepository; } @Override public void resume() { getAllCosts(); } @Override public void pause() { } @Override public void stop() { } @Override public void destroy() { } @Override public void onError(String message) { } @Override public void getAllCosts() { // get all costs GetAllCostsInteractor getCostsInteractor = new GetAllCostsInteractorImpl( mExecutor, mMainThread, mCostRepository, this ); getCostsInteractor.execute(); } @Override public void onCostsRetrieved(List costList) { List dailyTotalCosts = DailyTotalCostConverter.convertCostsToDailyCosts(costList); mView.showCosts(dailyTotalCosts); } @Override public void deleteCost(long costId) { // delete this cost item in a background thread DeleteCostInteractor deleteCostInteractor = new DeleteCostInteractorImpl( mExecutor, mMainThread, costId, this, mCostRepository ); deleteCostInteractor.execute(); } @Override public void onCostDeleted(Cost cost) { mView.onCostDeleted(cost); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/BaseView.java ================================================ package com.kodelabs.mycosts.presentation.ui; /** * Created by dmilicic on 7/28/15. *

* This interface represents a basic view. All views should implement these common methods. */ public interface BaseView { /** * This is a general method used for showing some kind of progress during a background task. For example, this * method should show a progress bar and/or disable buttons before some background work starts. */ void showProgress(); /** * This is a general method used for hiding progress information after a background task finishes. */ void hideProgress(); /** * This method is used for showing error messages on the UI. * * @param message The error message to be dislayed. */ void showError(String message); } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/activities/AboutActivity.java ================================================ package com.kodelabs.mycosts.presentation.ui.activities; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.support.design.widget.FloatingActionButton; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.view.View; import com.kodelabs.mycosts.R; public class AboutActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_about); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { // let the user choose his email client Intent emailIntent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts( "mailto", "dario.milicic@gmail.com", null)); startActivity(Intent.createChooser(emailIntent, "Send email...")); } }); getSupportActionBar().setDisplayHomeAsUpEnabled(true); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/activities/AbstractCostActivity.java ================================================ package com.kodelabs.mycosts.presentation.ui.activities; import android.app.DatePickerDialog; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.view.Menu; import android.view.View; import android.view.View.OnClickListener; import android.widget.DatePicker; import android.widget.EditText; import android.widget.Spinner; import android.widget.TextView; import com.kodelabs.mycosts.R; import com.kodelabs.mycosts.presentation.ui.fragments.DatePickerFragment; import com.kodelabs.mycosts.utils.DateUtils; import java.util.Date; import butterknife.Bind; import butterknife.ButterKnife; import butterknife.OnClick; import io.codetail.widget.RevealFrameLayout; /** * Created by dmilicic on 12/26/15. */ public abstract class AbstractCostActivity extends AppCompatActivity implements DatePickerDialog.OnDateSetListener { @Bind(R.id.reveal_layout) RevealFrameLayout mRevealLayout; @Bind(R.id.toolbar) Toolbar mToolbar; @Bind(R.id.input_date) TextView mDateTextView; @Bind(R.id.input_amount) EditText mAmountEditText; @Bind(R.id.input_description) EditText mDescriptionEditText; @Bind(R.id.input_cost_category) Spinner mCategorySpinner; protected Date mSelectedDate; protected String mDescription; protected String mCategory; protected double mAmount; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_add_cost); ButterKnife.bind(this); mToolbar.setNavigationIcon(R.drawable.ic_close_white_24dp); setSupportActionBar(mToolbar); mToolbar.setNavigationOnClickListener(new OnClickListener() { @Override public void onClick(View v) { onBackPressed(); } }); mRevealLayout.setVisibility(View.VISIBLE); } @OnClick(R.id.input_date) public void showDatePickerDialog(View v) { DatePickerFragment newFragment = new DatePickerFragment(); newFragment.setListener(this); newFragment.show(getFragmentManager(), "datePicker"); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_add_cost, menu); return true; } protected void extractFormData() { // extract data from the form try { mAmount = Double.valueOf(mAmountEditText.getText().toString()); } catch (NumberFormatException e) { mAmount = 0.0; } // extract description and category mDescription = mDescriptionEditText.getText().toString(); mCategory = mCategorySpinner.getSelectedItem().toString(); } @Override public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) { mSelectedDate = DateUtils.createDate(year, monthOfYear, dayOfMonth); mDateTextView.setText(DateUtils.formatDate(mSelectedDate)); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/activities/AddCostActivity.java ================================================ package com.kodelabs.mycosts.presentation.ui.activities; import android.os.Bundle; import android.view.MenuItem; import android.widget.Toast; import com.kodelabs.mycosts.R; import com.kodelabs.mycosts.domain.executor.impl.ThreadExecutor; import com.kodelabs.mycosts.presentation.presenters.AddCostPresenter; import com.kodelabs.mycosts.presentation.presenters.impl.AddCostPresenterImpl; import com.kodelabs.mycosts.storage.CostRepositoryImpl; import com.kodelabs.mycosts.threading.MainThreadImpl; import com.kodelabs.mycosts.utils.DateUtils; public class AddCostActivity extends AbstractCostActivity implements AddCostPresenter.View { private AddCostPresenter mPresenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // setup the presenter mPresenter = new AddCostPresenterImpl( ThreadExecutor.getInstance(), MainThreadImpl.getInstance(), this, new CostRepositoryImpl(this) ); } @Override protected void onResume() { super.onResume(); // default day should be today mSelectedDate = DateUtils.getToday(); mDateTextView.setText(DateUtils.formatDate(mSelectedDate)); } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_save) { extractFormData(); // pass the data onto the presenter mPresenter.addNewCost(mSelectedDate, mAmount, mDescription, mCategory); return true; } return super.onOptionsItemSelected(item); } @Override public void onCostAdded() { Toast.makeText(this, "Saved!", Toast.LENGTH_LONG).show(); onBackPressed(); } @Override public void showProgress() { } @Override public void hideProgress() { } @Override public void showError(String message) { } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/activities/EditCostActivity.java ================================================ package com.kodelabs.mycosts.presentation.ui.activities; import android.content.Intent; import android.os.Bundle; import android.support.annotation.NonNull; import android.view.MenuItem; import android.widget.Toast; import com.kodelabs.mycosts.R; import com.kodelabs.mycosts.domain.executor.impl.ThreadExecutor; import com.kodelabs.mycosts.domain.model.Cost; import com.kodelabs.mycosts.presentation.presenters.EditCostPresenter; import com.kodelabs.mycosts.presentation.presenters.impl.EditCostPresenterImpl; import com.kodelabs.mycosts.storage.CostRepositoryImpl; import com.kodelabs.mycosts.threading.MainThreadImpl; import com.kodelabs.mycosts.utils.DateUtils; /** * Created by dmilicic on 12/27/15. */ public class EditCostActivity extends AbstractCostActivity implements EditCostPresenter.View { private EditCostPresenter mPresenter; private Cost mEditedCost; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // create a presenter for this screen mPresenter = new EditCostPresenterImpl( ThreadExecutor.getInstance(), MainThreadImpl.getInstance(), this, new CostRepositoryImpl(this) ); // extract the cost id of the item we want to edit long costId = getIntent().getLongExtra(MainActivity.EXTRA_COST_ID, -1); // in case cost id is not sent if (costId == -1) { Toast.makeText(this, "Cost not found!", Toast.LENGTH_SHORT).show(); finish(); return; } // first get the old cost from the database mPresenter.getCostById(costId); } @Override public void onCostRetrieved(@NonNull Cost cost) { mEditedCost = cost; // populate the member variables mAmount = cost.getAmount(); mSelectedDate = cost.getDate(); mCategory = cost.getCategory(); mDescription = cost.getDescription(); prepopulateFields(); } /** * Finds the position of the given category inside the spinner. * * @param category The provided category for which we are finding the position. * @return Returns an int which represents a position of the provided category in the spinner. */ private int findCategoryPosition(String category) { int result = -1; String[] categoryArray = getResources().getStringArray(R.array.category_array); for (int i = 0; i < categoryArray.length; i++) { if (category.equals(categoryArray[i])) result = i; } return result; } private void prepopulateFields() { // prepopulate all text views mAmountEditText.setText(String.valueOf(mAmount)); mDateTextView.setText(DateUtils.formatDate(mSelectedDate)); mDescriptionEditText.setText(mDescription); // find the position of the category item to select int position = findCategoryPosition(mCategory); mCategorySpinner.setSelection(position); } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_save) { extractFormData(); // pass the data onto the presenter mPresenter.editCost(mEditedCost, mSelectedDate, mAmount, mDescription, mCategory); return true; } return super.onOptionsItemSelected(item); } @Override public void onCostUpdated(Cost cost) { // build the data to send Intent data = new Intent(); data.putExtra(MainActivity.EXTRA_COST_ID, cost.getId()); // mark that this was a success setResult(RESULT_OK, data); finish(); } @Override public void showProgress() { } @Override public void hideProgress() { } @Override public void showError(String message) { Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/activities/MainActivity.java ================================================ package com.kodelabs.mycosts.presentation.ui.activities; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Toast; import com.kodelabs.mycosts.R; import com.kodelabs.mycosts.domain.executor.impl.ThreadExecutor; import com.kodelabs.mycosts.domain.model.Cost; import com.kodelabs.mycosts.presentation.animation.AnimatorFactory; import com.kodelabs.mycosts.presentation.model.DailyTotalCost; import com.kodelabs.mycosts.presentation.presenters.MainPresenter; import com.kodelabs.mycosts.presentation.presenters.impl.MainPresenterImpl; import com.kodelabs.mycosts.presentation.ui.adapters.CostItemAdapter; import com.kodelabs.mycosts.storage.CostRepositoryImpl; import com.kodelabs.mycosts.sync.auth.DummyAccountProvider; import com.kodelabs.mycosts.threading.MainThreadImpl; import java.util.List; import butterknife.Bind; import butterknife.ButterKnife; import io.codetail.widget.RevealFrameLayout; import timber.log.Timber; public class MainActivity extends AppCompatActivity implements MainPresenter.View { public static final String EXTRA_COST_ID = "extra_cost_id_key"; public static final int EDIT_COST_REQUEST = 0; @Bind(R.id.expenses_list) RecyclerView mRecyclerView; @Bind(R.id.reveal_layout) RevealFrameLayout mRevealLayout; private MainPresenter mMainPresenter; private CostItemAdapter mAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); Timber.w("ONCREATE"); init(); } private void init() { // setup recycler view adapter mAdapter = new CostItemAdapter(this, this); // setup recycler view mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); mRecyclerView.setAdapter(mAdapter); // setup toolbar Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); // instantiate the presenter mMainPresenter = new MainPresenterImpl( ThreadExecutor.getInstance(), MainThreadImpl.getInstance(), this, new CostRepositoryImpl(this) ); // create a dummy account if it doesn't yet exist DummyAccountProvider.CreateSyncAccount(this); } @Override protected void onResume() { super.onResume(); mMainPresenter.resume(); // reset the layout mRevealLayout.setVisibility(View.INVISIBLE); Timber.w("ONRESUME"); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); switch (id) { case R.id.action_add_cost: // intent to start another activity final Intent intent = new Intent(MainActivity.this, AddCostActivity.class); // do the animation AnimatorFactory.enterReveal(mRevealLayout, intent, MainActivity.this); break; case R.id.action_about: final Intent aboutIntent = new Intent(MainActivity.this, AboutActivity.class); startActivity(aboutIntent); break; default: break; } return super.onOptionsItemSelected(item); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); // check if everything is ok if (requestCode == EDIT_COST_REQUEST && resultCode == RESULT_OK) { // let the user know the edit succeded Toast.makeText(this, "Updated!", Toast.LENGTH_LONG).show(); } } @Override public void showCosts(List costs) { // signal the adapter that it has data to show mAdapter.addNewCosts(costs); } @Override public void onClickDeleteCost(final long costId) { DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { switch (which) { case DialogInterface.BUTTON_POSITIVE: mMainPresenter.deleteCost(costId); break; case DialogInterface.BUTTON_NEGATIVE: //No button clicked break; } } }; AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage("Delete this cost?") .setPositiveButton("Yes", dialogClickListener) .setNegativeButton("No", dialogClickListener) .show(); } @Override public void onCostDeleted(Cost cost) { // we deleted some data, RELOAD ALL THE THINGS! mMainPresenter.getAllCosts(); } @Override public void onClickEditCost(long costId, int position) { // intent to start another activity final Intent intent = new Intent(MainActivity.this, EditCostActivity.class); intent.putExtra(EXTRA_COST_ID, costId); startActivityForResult(intent, EDIT_COST_REQUEST); } @Override public void showProgress() { } @Override public void hideProgress() { } @Override public void showError(String message) { } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/adapters/CostItemAdapter.java ================================================ package com.kodelabs.mycosts.presentation.ui.adapters; import android.content.Context; import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import com.kodelabs.mycosts.R; import com.kodelabs.mycosts.presentation.model.DailyTotalCost; import com.kodelabs.mycosts.presentation.presenters.MainPresenter; import com.kodelabs.mycosts.presentation.ui.customviews.ExpandedCostView; import com.kodelabs.mycosts.presentation.ui.listeners.IndividualCostViewClickListener; import com.kodelabs.mycosts.presentation.ui.listeners.RecyclerViewClickListener; import com.kodelabs.mycosts.utils.DateUtils; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import butterknife.Bind; import butterknife.ButterKnife; /** * Created by dmilicic on 12/13/15. */ public class CostItemAdapter extends RecyclerView.Adapter implements RecyclerViewClickListener { private enum ViewType { CONTRACTED_CARD, EXPANDED_CARD } private List mCostList; private Context mContext; private Set mSelectedItems; public final MainPresenter.View mView; public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { @Bind(R.id.cost_item_title) public TextView mTitle; @Bind(R.id.cost_item_total_value) public TextView mTotalCost; private RecyclerViewClickListener mListener; public void setup(DailyTotalCost dailyTotalCost) { Context context = mTitle.getContext(); final String dateText = DateUtils.dateToText(context, dailyTotalCost.getDate()); final String title = String.format(context.getString(R.string.total_expenses), dateText); mTitle.setText(title); mTotalCost.setText(String.valueOf(dailyTotalCost.getTotalCost()) + "$"); } @Override public void onClick(View v) { mListener.onClickView(getAdapterPosition()); } public ViewHolder(View v, final RecyclerViewClickListener listener) { super(v); ButterKnife.bind(this, v); v.setOnClickListener(this); mListener = listener; } } public static class ExpandedViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, IndividualCostViewClickListener { @Bind(R.id.card_expanded_costview) public ExpandedCostView mExpandedCostView; private RecyclerViewClickListener mListener; @Override public void onClickDelete(long costId) { mListener.onClickDelete(getAdapterPosition(), costId); } @Override public void onClickEdit(long costId) { mListener.onClickEdit(getAdapterPosition(), costId); } @Override public void onClick(View v) { mListener.onClickView(getAdapterPosition()); } public ExpandedViewHolder(View v, final RecyclerViewClickListener listener) { super(v); ButterKnife.bind(this, v); v.setOnClickListener(this); // this listener is our adapter mListener = listener; // set a listener for edit, delete calls mExpandedCostView.setIndividualCostViewClickListener(this); } } public CostItemAdapter(MainPresenter.View view, Context context) { mCostList = new ArrayList<>(); mView = view; mContext = context; mSelectedItems = new HashSet<>(); } @Override public int getItemViewType(int position) { // check to see if a view at this position should be expanded or normal/contracted if (mSelectedItems.contains(position)) return ViewType.EXPANDED_CARD.ordinal(); return ViewType.CONTRACTED_CARD.ordinal(); } @Override public void onClickView(int position) { // If clicked on for the first time the view should be counted as selected, if clicked again the view // should be considered unselected. // Selected views will be shown as expanded cards while unselected will be shown as normal/contracted cards if (!mSelectedItems.contains(position)) mSelectedItems.add(position); else mSelectedItems.remove(position); notifyItemChanged(position); } @Override public void onClickDelete(int position, long costId) { // in case we are deleting the last element from a day, mark that day as unselected, no point in showing an empty day if (mSelectedItems.contains(position) && mCostList.get(position).getCostList().size() == 1) mSelectedItems.remove(position); mView.onClickDeleteCost(costId); } @Override public void onClickEdit(int position, long costId) { mView.onClickEditCost(costId, position); } public void addNewCosts(@NonNull List costList) { // clean up old data if (mCostList != null) { mCostList.clear(); } mCostList = costList; notifyDataSetChanged(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { Context context = parent.getContext(); LayoutInflater inflater = LayoutInflater.from(context); // check if this should be an expanded card if (viewType == ViewType.EXPANDED_CARD.ordinal()) { View view = inflater.inflate(R.layout.card_expanded_daily_cost_item, parent, false); return new ExpandedViewHolder(view, this); } // this is a normal/contracted card View view = inflater.inflate(R.layout.card_daily_cost_item, parent, false); return new ViewHolder(view, this); } @Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { DailyTotalCost cost = mCostList.get(position); // setup the views depending on the viewholder type if (viewHolder instanceof ViewHolder) { ((ViewHolder) viewHolder).setup(cost); } else if (viewHolder instanceof ExpandedViewHolder) { ((ExpandedViewHolder) viewHolder).mExpandedCostView.setDailyCost(cost); } } @Override public int getItemCount() { return mCostList.size(); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/customviews/CostItemView.java ================================================ package com.kodelabs.mycosts.presentation.ui.customviews; import android.content.Context; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.widget.ImageButton; import android.widget.PopupMenu; import android.widget.PopupMenu.OnMenuItemClickListener; import android.widget.RelativeLayout; import android.widget.TextView; import com.kodelabs.mycosts.R; import com.kodelabs.mycosts.domain.model.Cost; import com.kodelabs.mycosts.presentation.ui.listeners.IndividualCostViewClickListener; import butterknife.Bind; import butterknife.ButterKnife; import butterknife.OnClick; /** * Created by dmilicic on 1/6/16. */ public class CostItemView extends RelativeLayout implements OnMenuItemClickListener { @Bind(R.id.cost_item_title) TextView mCategoryView; @Bind(R.id.cost_item_total_value) TextView mValueView; @Bind(R.id.cost_item_description) TextView mDescriptionView; @Bind(R.id.button_menu) ImageButton mMenuButton; private IndividualCostViewClickListener mCostViewClickListener; private Cost mCost; public CostItemView(Context context, IndividualCostViewClickListener costViewClickListener, Cost cost) { super(context); mCostViewClickListener = costViewClickListener; mCost = cost; init(context); } private void init(Context context) { LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); View view = inflater.inflate(R.layout.individual_cost_item, this); ButterKnife.bind(this, view); setCategory(mCost.getCategory()); setDescription(mCost.getDescription()); setValue(mCost.getAmount()); } @Override public boolean onMenuItemClick(MenuItem item) { // since the listener is set after this object is created it is possible that it can be null, avoid that :) if (mCostViewClickListener == null) return false; switch (item.getItemId()) { case R.id.item_edit: mCostViewClickListener.onClickEdit(mCost.getId()); return true; case R.id.item_delete: mCostViewClickListener.onClickDelete(mCost.getId()); return true; default: return false; } } @OnClick(R.id.button_menu) void onClickMenu() { PopupMenu popupMenu = new PopupMenu(getContext(), mMenuButton); popupMenu.setOnMenuItemClickListener(this); popupMenu.inflate(R.menu.menu_cost_item); popupMenu.show(); } private void setCategory(String category) { mCategoryView.setText(category); } private void setValue(double value) { String val = String.format("%.2f$", value); mValueView.setText(val); } private void setDescription(String description) { mDescriptionView.setText(description); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/customviews/ExpandedCostView.java ================================================ package com.kodelabs.mycosts.presentation.ui.customviews; import android.content.Context; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.support.v7.widget.CardView; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; import com.kodelabs.mycosts.R; import com.kodelabs.mycosts.domain.model.Cost; import com.kodelabs.mycosts.presentation.model.DailyTotalCost; import com.kodelabs.mycosts.presentation.ui.listeners.IndividualCostViewClickListener; import com.kodelabs.mycosts.utils.DateUtils; import java.util.Date; import java.util.List; import butterknife.Bind; import butterknife.ButterKnife; /** * Created by dmilicic on 1/6/16. */ public class ExpandedCostView extends CardView { @Bind(R.id.cost_item_title) TextView mTitle; @Bind(R.id.cost_item_total_value) TextView mValue; @Bind(R.id.layout_individual_cost_items) LinearLayout mLinearLayout; @Nullable private IndividualCostViewClickListener mIndividualCostViewClickListener; public ExpandedCostView(Context context) { super(context); init(context); } public ExpandedCostView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public ExpandedCostView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } private void init(Context context) { LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); View view = inflater.inflate(R.layout.expanded_cost_item, this); ButterKnife.bind(this, view); } public void setIndividualCostViewClickListener( @Nullable IndividualCostViewClickListener individualCostViewClickListener) { mIndividualCostViewClickListener = individualCostViewClickListener; } private void addCostItem(Cost cost, int position) { CostItemView costView = new CostItemView(getContext(), mIndividualCostViewClickListener, cost); // every other cost item will have a different background so its easier on the eyes if (position % 2 == 0) { costView.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.colorBrightGray)); } mLinearLayout.addView(costView); } private void setTitle(Date date) { final String dateText = DateUtils.dateToText(getContext(), date); final String title = String.format(getContext().getString(R.string.total_expenses), dateText); mTitle.setText(title); } private void setTotalValue(double value) { String val = String.format("%.2f$", value); mValue.setText(val); } public void setDailyCost(DailyTotalCost dailyCost) { // reset the individual cost items mLinearLayout.removeAllViews(); // convert date to text setTitle(dailyCost.getDate()); setTotalValue(dailyCost.getTotalCost()); // add the individual cost items List costList = dailyCost.getCostList(); Cost cost; for (int idx = 0; idx < costList.size(); idx++) { cost = costList.get(idx); addCostItem(cost, idx); } } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/fragments/DatePickerFragment.java ================================================ package com.kodelabs.mycosts.presentation.ui.fragments; import android.app.DatePickerDialog; import android.app.DatePickerDialog.OnDateSetListener; import android.app.Dialog; import android.app.DialogFragment; import android.os.Bundle; import java.util.Calendar; /** * Created by dmilicic on 12/20/15. */ public class DatePickerFragment extends DialogFragment { public DatePickerFragment() { // empty } private DatePickerDialog.OnDateSetListener mListener; public void setListener(OnDateSetListener listener) { mListener = listener; } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { // Use the current date as the default date in the picker final Calendar c = Calendar.getInstance(); int year = c.get(Calendar.YEAR); int month = c.get(Calendar.MONTH); int day = c.get(Calendar.DAY_OF_MONTH); // Create a new instance of DatePickerDialog and return it return new DatePickerDialog(getActivity(), mListener, year, month, day); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/listeners/IndividualCostViewClickListener.java ================================================ package com.kodelabs.mycosts.presentation.ui.listeners; /** * Created by dmilicic on 1/6/16. */ public interface IndividualCostViewClickListener { void onClickDelete(long costId); void onClickEdit(long costId); } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/listeners/RecyclerViewClickListener.java ================================================ package com.kodelabs.mycosts.presentation.ui.listeners; /** * Created by dmilicic on 12/26/15. */ public interface RecyclerViewClickListener { void onClickView(int position); void onClickEdit(int position, long costId); void onClickDelete(int position, long costId); } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/storage/CostRepositoryImpl.java ================================================ package com.kodelabs.mycosts.storage; import android.content.Context; import com.kodelabs.mycosts.domain.model.Cost; import com.kodelabs.mycosts.domain.repository.CostRepository; import com.kodelabs.mycosts.storage.converters.StorageModelConverter; import com.kodelabs.mycosts.storage.model.Cost_Table; import com.kodelabs.mycosts.sync.SyncAdapter; import com.kodelabs.mycosts.utils.DateUtils; import com.raizlabs.android.dbflow.sql.language.SQLite; import java.util.Calendar; import java.util.Date; import java.util.List; /** * Created by dmilicic on 12/13/15. */ public class CostRepositoryImpl implements CostRepository { private Context mContext; // let's generate some dummy data static { List costs = SQLite.select() .from(com.kodelabs.mycosts.storage.model.Cost.class) .queryList(); // if the database is empty, let's add some dummies if (costs.size() == 0) { // get the today's date for some sample cost items Calendar calendar = Calendar.getInstance(); Date today = calendar.getTime(); today = DateUtils.truncateHours(today); // set hours, minutes and seconds to 0 for simplicity // get yesterday as well calendar.add(Calendar.DATE, -1); Date yesterday = calendar.getTime(); yesterday = DateUtils.truncateHours(yesterday); // set hours, minutes and seconds to 0 for simplicity // Since each cost is uniquely identified by a timestamp, we should make sure that the sample costs are // not created in the same millisecond, we simply pause a bit after each cost creation. try { com.kodelabs.mycosts.storage.model.Cost cost = new com.kodelabs.mycosts.storage.model.Cost("Groceries", "Bought some X and some Y", today, 100.0); cost.insert(); Thread.sleep(100); cost = new com.kodelabs.mycosts.storage.model.Cost("Bills", "Bill for electricity", today, 50.0); cost.insert(); Thread.sleep(100); Thread.sleep(100); cost = new com.kodelabs.mycosts.storage.model.Cost("Transportation", "I took an Uber ride", yesterday, 10.0); cost.insert(); Thread.sleep(100); cost = new com.kodelabs.mycosts.storage.model.Cost("Entertainment", "I went to see Star Wars!", yesterday, 50.0); cost.insert(); } catch (InterruptedException e) { e.printStackTrace(); } } } public CostRepositoryImpl(Context context) { mContext = context; } @Override public void insert(Cost item) { com.kodelabs.mycosts.storage.model.Cost dbItem = StorageModelConverter.convertToStorageModel(item); // mark as unsynced dbItem.synced = false; dbItem.insert(); SyncAdapter.triggerSync(mContext); } @Override public void update(Cost cost) { com.kodelabs.mycosts.storage.model.Cost dbItem = StorageModelConverter.convertToStorageModel(cost); // mark as unsynced dbItem.synced = false; dbItem.update(); SyncAdapter.triggerSync(mContext); } @Override public Cost getCostById(long id) { com.kodelabs.mycosts.storage.model.Cost cost = SQLite .select() .from(com.kodelabs.mycosts.storage.model.Cost.class) .where(Cost_Table.id.eq(id)) .querySingle(); return StorageModelConverter.convertToDomainModel(cost); } @Override public List getAllCosts() { List costs = SQLite .select() .from(com.kodelabs.mycosts.storage.model.Cost.class) .queryList(); return StorageModelConverter.convertListToDomainModel(costs); } @Override public List getAllUnsyncedCosts() { List costs = SQLite .select() .from(com.kodelabs.mycosts.storage.model.Cost.class) .where(Cost_Table.synced.eq(false)) .queryList(); return StorageModelConverter.convertListToDomainModel(costs); } @Override public void markSynced(List costs) { // we have to convert it to the database model before storing List unsyncedCosts = StorageModelConverter.convertListToStorageModel(costs); for (com.kodelabs.mycosts.storage.model.Cost cost : unsyncedCosts) { cost.synced = true; cost.update(); } } @Override public void delete(Cost cost) { com.kodelabs.mycosts.storage.model.Cost dbItem = StorageModelConverter.convertToStorageModel(cost); dbItem.delete(); SyncAdapter.triggerSync(mContext); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/storage/contentprovider/StubProvider.java ================================================ package com.kodelabs.mycosts.storage.contentprovider; import android.content.ContentProvider; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; /** * Created by dmilicic on 2/11/16. * Define an implementation of ContentProvider that stubs out * all methods */ public class StubProvider extends ContentProvider { /* * Always return true, indicating that the * provider loaded correctly. */ @Override public boolean onCreate() { return true; } /* * Return no type for MIME type */ @Override public String getType(Uri uri) { return null; } /* * query() always returns no results * */ @Override public Cursor query( Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { return null; } /* * insert() always returns null (no URI) */ @Override public Uri insert(Uri uri, ContentValues values) { return null; } /* * delete() always returns "no rows affected" (0) */ @Override public int delete(Uri uri, String selection, String[] selectionArgs) { return 0; } /* * update() always returns "no rows affected" (0) */ public int update( Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/storage/converters/StorageModelConverter.java ================================================ package com.kodelabs.mycosts.storage.converters; import com.kodelabs.mycosts.storage.model.Cost; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * Created by dmilicic on 2/11/16. */ public class StorageModelConverter { public static Cost convertToStorageModel(com.kodelabs.mycosts.domain.model.Cost cost) { Cost result = new Cost(); result.setDescription(cost.getDescription()); result.setAmount(cost.getAmount()); result.setCategory(cost.getCategory()); result.setDate(cost.getDate()); result.setId(cost.getId()); return result; } public static com.kodelabs.mycosts.domain.model.Cost convertToDomainModel(Cost cost) { String desc = cost.getDescription(); double amount = cost.getAmount(); String category = cost.getCategory(); Date date = cost.getDate(); long id = cost.getId(); com.kodelabs.mycosts.domain.model.Cost result = new com.kodelabs.mycosts.domain.model.Cost( category, desc, date, amount, id ); return result; } public static List convertListToDomainModel(List costs) { List convertedCosts = new ArrayList<>(); for (Cost cost : costs) { convertedCosts.add(convertToDomainModel(cost)); } // cleanup costs.clear(); costs = null; return convertedCosts; } public static List convertListToStorageModel(List costs) { List convertedCosts = new ArrayList<>(); for (com.kodelabs.mycosts.domain.model.Cost cost : costs) { convertedCosts.add(convertToStorageModel(cost)); } // cleanup costs.clear(); costs = null; return convertedCosts; } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/storage/database/CostDatabase.java ================================================ package com.kodelabs.mycosts.storage.database; import com.raizlabs.android.dbflow.annotation.Database; /** * Created by dmilicic on 2/11/16. */ @Database(name = CostDatabase.NAME, version = CostDatabase.VERSION) public class CostDatabase { public static final String NAME = "Costs_db"; public static final int VERSION = 1; } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/storage/model/Cost.java ================================================ package com.kodelabs.mycosts.storage.model; import com.kodelabs.mycosts.storage.database.CostDatabase; import com.raizlabs.android.dbflow.annotation.Column; import com.raizlabs.android.dbflow.annotation.PrimaryKey; import com.raizlabs.android.dbflow.annotation.Table; import com.raizlabs.android.dbflow.structure.BaseModel; import java.util.Date; /** * Created by dmilicic on 2/11/16. */ @Table(database = CostDatabase.class) public class Cost extends BaseModel { @PrimaryKey private long id; // our base model already has an id, let's use it as a primary key @Column private String category; @Column private String description; @Column private Date date; @Column private double amount; @Column public boolean synced; public Cost() { } /** * This constructor is only used to create some dummy objects when the app starts. */ public Cost(String category, String description, Date date, double amount) { // cost will be "uniquely" identified by the current timestamp this.id = new Date().getTime(); this.category = category; this.description = description; this.date = date; this.amount = amount; this.synced = false; } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getCategory() { return category; } public void setCategory(String category) { this.category = category; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public Date getDate() { return date; } public void setDate(Date date) { this.date = date; } public double getAmount() { return amount; } public void setAmount(double amount) { this.amount = amount; } @Override public String toString() { return "Cost{" + "id=" + id + ", category='" + category + '\'' + ", description='" + description + '\'' + ", date=" + date + ", amount=" + amount + '}'; } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/sync/SyncAdapter.java ================================================ package com.kodelabs.mycosts.sync; import android.accounts.Account; import android.content.AbstractThreadedSyncAdapter; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; import android.content.SyncResult; import android.os.Bundle; import com.kodelabs.mycosts.R; import com.kodelabs.mycosts.domain.model.Cost; import com.kodelabs.mycosts.domain.repository.CostRepository; import com.kodelabs.mycosts.network.RestClient; import com.kodelabs.mycosts.network.converters.RESTModelConverter; import com.kodelabs.mycosts.network.model.Payload; import com.kodelabs.mycosts.network.model.RESTCost; import com.kodelabs.mycosts.network.services.SyncService; import com.kodelabs.mycosts.storage.CostRepositoryImpl; import com.kodelabs.mycosts.utils.AuthUtils; import java.io.IOException; import java.util.List; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import timber.log.Timber; /** *  * Handle the transfer of data between a server and an *  * app, using the Android sync adapter framework. *   */ public class SyncAdapter extends AbstractThreadedSyncAdapter { private Context mContext; private CostRepository mCostRepository; private List mUnsyncedCosts; public SyncAdapter(Context context, boolean autoInitialize) { super(context, autoInitialize); mContext = context; mCostRepository = new CostRepositoryImpl(mContext); } public SyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs) { super(context, autoInitialize, allowParallelSyncs); mContext = context; mCostRepository = new CostRepositoryImpl(mContext); } /** * This method will start a sync adapter that will upload data to the server. */ public static void triggerSync(Context context) { // TODO sync adapter is forced for debugging purposes, remove this in production // Pass the settings flags by inserting them in a bundle Bundle settingsBundle = new Bundle(); settingsBundle.putBoolean( ContentResolver.SYNC_EXTRAS_MANUAL, true); settingsBundle.putBoolean( ContentResolver.SYNC_EXTRAS_EXPEDITED, true); // request a sync using sync adapter Account account = AuthUtils.getAccount(context); ContentResolver.requestSync(account, context.getString(R.string.stub_content_authority), settingsBundle); } private Callback mResponseCallback = new Callback() { @Override public void onResponse(Call call, Response response) { Timber.i("UPLOAD SUCCESS: %d", response.code()); if (response.isSuccess()) { mCostRepository.markSynced(mUnsyncedCosts); } } @Override public void onFailure(Call call, Throwable t) { Timber.e("UPLOAD FAIL"); t.printStackTrace(); // try to sync again SyncResult syncResult = new SyncResult(); } }; @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { Timber.i("STARTING SYNC..."); // initialize the services we will use SyncService syncService = RestClient.getService(SyncService.class); // TODO: get the real user's name Payload payload = new Payload("default"); // get all unsynced data mUnsyncedCosts = mCostRepository.getAllUnsyncedCosts(); for (Cost cost : mUnsyncedCosts) { // convert to models suitable for transferring over network RESTCost restCost = RESTModelConverter.convertToRestModel(cost); payload.addCost(restCost); } // run the upload try { Response response = syncService.uploadData(payload).execute(); Timber.i("UPLOAD SUCCESS: %d", response.code()); // everything went well, mark local cost items as synced if (response.isSuccess()) { mCostRepository.markSynced(mUnsyncedCosts); } } catch (IOException e) { // something went wrong Timber.e("UPLOAD FAIL"); // make it a soft error so the framework does the exponential backoff syncResult.stats.numIoExceptions += 1; Timber.d("Restarting sync in %d seconds", syncResult.delayUntil); } } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/sync/SyncService.java ================================================ package com.kodelabs.mycosts.sync; import android.app.Service; import android.content.Intent; import android.os.IBinder; import android.support.annotation.Nullable; /** * Created by dmilicic on 2/11/16. * This service is the component that connects our SyncAdapter to our application as it is in another process. */ public class SyncService extends Service { // Storage for an instance of the sync adapter private static SyncAdapter sSyncAdapter = null; // Object to use as a thread-safe lock private static final Object sSyncAdapterLock = new Object(); /* * Instantiate the sync adapter object. */ @Override public void onCreate() { /* * Create the sync adapter as a singleton. * Set the sync adapter as syncable * Disallow parallel syncs */ synchronized (sSyncAdapterLock) { if (sSyncAdapter == null) { sSyncAdapter = new SyncAdapter(getApplicationContext(), true); } } } /** * Return an object that allows the system to invoke * the sync adapter. */ @Nullable @Override public IBinder onBind(Intent intent) { /* * Get the object that allows external processes * to call onPerformSync(). The object is created * in the base class code when the SyncAdapter * constructors call super() */ return sSyncAdapter.getSyncAdapterBinder(); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/sync/auth/Authenticator.java ================================================ package com.kodelabs.mycosts.sync.auth; import android.accounts.AbstractAccountAuthenticator; import android.accounts.Account; import android.accounts.AccountAuthenticatorResponse; import android.accounts.NetworkErrorException; import android.content.Context; import android.os.Bundle; /** * Implement AbstractAccountAuthenticator and stub out all * of its methods *

* We need at least a stub Android authentication so we can use the Android Sync adapter mechanism. */ public class Authenticator extends AbstractAccountAuthenticator { // Simple constructor public Authenticator(Context context) { super(context); } // Editing properties is not supported @Override public Bundle editProperties( AccountAuthenticatorResponse r, String s) { throw new UnsupportedOperationException(); } // Don't add additional accounts @Override public Bundle addAccount( AccountAuthenticatorResponse r, String s, String s2, String[] strings, Bundle bundle) throws NetworkErrorException { return null; } // Ignore attempts to confirm credentials @Override public Bundle confirmCredentials( AccountAuthenticatorResponse r, Account account, Bundle bundle) throws NetworkErrorException { return null; } // Getting an authentication token is not supported @Override public Bundle getAuthToken( AccountAuthenticatorResponse r, Account account, String s, Bundle bundle) throws NetworkErrorException { throw new UnsupportedOperationException(); } // Getting a label for the auth token is not supported @Override public String getAuthTokenLabel(String s) { throw new UnsupportedOperationException(); } // Updating user credentials is not supported @Override public Bundle updateCredentials( AccountAuthenticatorResponse r, Account account, String s, Bundle bundle) throws NetworkErrorException { throw new UnsupportedOperationException(); } // Checking features for the account is not supported @Override public Bundle hasFeatures( AccountAuthenticatorResponse r, Account account, String[] strings) throws NetworkErrorException { throw new UnsupportedOperationException(); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/sync/auth/AuthenticatorService.java ================================================ package com.kodelabs.mycosts.sync.auth; import android.app.Service; import android.content.Intent; import android.os.IBinder; /** * A bound Service that instantiates the authenticator * when started. */ public class AuthenticatorService extends Service { /** * Instance field that stores the authenticator object */ private Authenticator mAuthenticator; @Override public void onCreate() { // Create a new authenticator object mAuthenticator = new Authenticator(this); } /** * When the system binds to this Service to make the RPC call * return the authenticator's IBinder. */ @Override public IBinder onBind(Intent intent) { return mAuthenticator.getIBinder(); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/sync/auth/DummyAccountProvider.java ================================================ package com.kodelabs.mycosts.sync.auth; import android.accounts.Account; import android.accounts.AccountManager; import android.content.Context; import com.kodelabs.mycosts.R; /** * Created by dmilicic on 2/11/16. */ public class DummyAccountProvider { public static Account getDummyAccount(Context context) { final String ACCOUNT = "dummyaccount"; final String ACCOUNT_TYPE = context.getString(R.string.account_type); final String AUTHORITY = context.getString(R.string.stub_content_authority); // Create the account type and default account return new Account(ACCOUNT, ACCOUNT_TYPE); } /** * Create a new dummy account for the sync adapter * * @param context The application context */ public static boolean CreateSyncAccount(Context context) { Account newAccount = getDummyAccount(context); // Get an instance of the Android account manager AccountManager accountManager = (AccountManager) context.getSystemService( Context.ACCOUNT_SERVICE); /* * Add the account and account type, no password or user data * If successful, return the Account object, otherwise report an error. */ return accountManager.addAccountExplicitly(newAccount, null, null); } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/threading/MainThreadImpl.java ================================================ package com.kodelabs.mycosts.threading; import android.os.Handler; import android.os.Looper; import com.kodelabs.mycosts.domain.executor.MainThread; /** * This class makes sure that the runnable we provide will be run on the main UI thread. *

* Created by dmilicic on 7/29/15. */ public class MainThreadImpl implements MainThread { private static MainThread sMainThread; private Handler mHandler; private MainThreadImpl() { mHandler = new Handler(Looper.getMainLooper()); } @Override public void post(Runnable runnable) { mHandler.post(runnable); } public static MainThread getInstance() { if (sMainThread == null) { sMainThread = new MainThreadImpl(); } return sMainThread; } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/utils/AuthUtils.java ================================================ package com.kodelabs.mycosts.utils; import android.accounts.Account; import android.accounts.AccountManager; import android.content.Context; import com.kodelabs.mycosts.R; import timber.log.Timber; /** * Created by dmilicic on 8/11/15. *

* This class will store useful utility methods for managing accounts on Android. */ public class AuthUtils { /** * Create a new dummy account needed by the sync adapter. */ public static Account createDummyAccount(Context context) { String accountName = "DummyAccount"; String accountType = context.getString(R.string.account_type); // Create the account type and default account Account newAccount = new Account(accountName, accountType); // Get an instance of the Android account manager AccountManager accountManager = (AccountManager) context.getSystemService( Context.ACCOUNT_SERVICE); /* * Add the account and account type, no password or user data * If successful, return the Account object, otherwise report an error. */ if (accountManager.addAccountExplicitly(newAccount, null, null)) { Timber.i("Account created!"); } else { /* * The account exists or some other error occurred. */ Timber.e("Account could not be created!"); return null; } return newAccount; } /** * Retrieves an account from the Android system if it exists. * * @param context The context of the application. * @return Returns an existing account or throws an exception if no accounts exist. */ public static Account getAccount(Context context) { if (context == null) throw new IllegalArgumentException("Context is null!"); // Get an instance of the Android account manager AccountManager accountManager = (AccountManager) context.getSystemService( Context.ACCOUNT_SERVICE); String accountType = context.getString(R.string.account_type); Account[] accounts = accountManager.getAccountsByType(accountType); if (accounts.length == 0) throw new IllegalStateException("There are is no account at all!"); // return the one and only account return accounts[0]; } } ================================================ FILE: app/src/main/java/com/kodelabs/mycosts/utils/DateUtils.java ================================================ package com.kodelabs.mycosts.utils; import android.content.Context; import com.kodelabs.mycosts.R; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.Locale; /** * Created by dmilicic on 9/20/15. */ public class DateUtils { /** * Converts a date to the textual representation of dates used by people. * * @param date * @return If the date is of today, then this method will return 'Today's'. If its yesterday then 'Yesterday' is returned. * Otherwise it returns the date in the form of dd.mm */ public static String dateToText(Context context, Date date) { String textDate; // clear hours, minutes and smaller time units from the date date = truncateHours(date); Calendar c = Calendar.getInstance(); // set the calendar to start of today c.set(Calendar.HOUR_OF_DAY, 0); c.set(Calendar.MINUTE, 0); c.set(Calendar.SECOND, 0); c.set(Calendar.MILLISECOND, 0); // and get that as a Date Date today = c.getTime(); // get yesterday c.add(Calendar.DATE, -1); Date yesterday = c.getTime(); if (date.equals(today)) { // test if today textDate = context.getString(R.string.today_s); } else if (date.equals(yesterday)) { // test if yesterday textDate = context.getString(R.string.yesterday_s); } else { textDate = formatDate(date, new SimpleDateFormat("dd.MM")); } return textDate; } public static Date createDate(int year, int monthOfYear, int dayOfMonth) { Calendar c = Calendar.getInstance(); // set the calendar to start of today c.set(Calendar.HOUR_OF_DAY, 0); c.set(Calendar.MINUTE, 0); c.set(Calendar.SECOND, 0); c.set(Calendar.MILLISECOND, 0); // setup the date c.set(Calendar.YEAR, year); c.set(Calendar.MONTH, monthOfYear); c.set(Calendar.DAY_OF_MONTH, dayOfMonth); // and get that as a Date Date resultDate = c.getTime(); return resultDate; } public static String formatDate(Date date) { SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yy", Locale.US); return sdf.format(date); } public static String formatDate(Date date, SimpleDateFormat sdf) { return sdf.format(date); } public static Date getToday() { Calendar c = Calendar.getInstance(); // set the calendar to start of today c.set(Calendar.HOUR_OF_DAY, 0); c.set(Calendar.MINUTE, 0); c.set(Calendar.SECOND, 0); c.set(Calendar.MILLISECOND, 0); // and get that as a Date Date today = c.getTime(); return today; } public static Date truncateHours(Date date) { Calendar c = Calendar.getInstance(); // set the calendar to start of today c.setTime(date); c.set(Calendar.HOUR_OF_DAY, 0); c.set(Calendar.MINUTE, 0); c.set(Calendar.SECOND, 0); c.set(Calendar.MILLISECOND, 0); // and get that as a Date return c.getTime(); } } ================================================ FILE: app/src/main/res/anim/hold.xml ================================================ ================================================ FILE: app/src/main/res/drawable/rounded_corner.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_about.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_add_cost.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: app/src/main/res/layout/card_daily_cost_item.xml ================================================ ================================================ FILE: app/src/main/res/layout/card_expanded_daily_cost_item.xml ================================================ ================================================ FILE: app/src/main/res/layout/content_about.xml ================================================ ================================================ FILE: app/src/main/res/layout/content_add_cost.xml ================================================ ================================================ FILE: app/src/main/res/layout/expanded_cost_item.xml ================================================ ================================================ FILE: app/src/main/res/layout/individual_cost_item.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_add_cost.xml ================================================

================================================ FILE: app/src/main/res/menu/menu_cost_item.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_main.xml ================================================ ================================================ FILE: app/src/main/res/values/colors.xml ================================================ #3F51B5 #303F9F #FF4081 #f1f1f1 ================================================ FILE: app/src/main/res/values/dimens.xml ================================================ 16dp 16dp 16dp ================================================ FILE: app/src/main/res/values/strings.xml ================================================ My Costs About Add new cost Today\'s Yesterday\'s %1$s total costs %1$s cost Made by Dario Milicic www.kodelabs.co Add cost About com.kodelabs.mycosts.account com.kodelabs.mycosts.provider Entertainment Equipment Gifts Groceries Insurance Medical Payment Rent Salary Shopping Tickets Transfer Transportation Utilities Bills Other ================================================ FILE: app/src/main/res/values/styles.xml ================================================ ================================================ FILE: app/src/main/res/values-w820dp/dimens.xml ================================================ 64dp ================================================ FILE: app/src/main/res/xml/authenticator.xml ================================================ ================================================ FILE: app/src/main/res/xml/syncadapter.xml ================================================ ================================================ FILE: app/src/test/java/com/kodelabs/mycosts/domain/interactors/GetCostByIdTest.java ================================================ package com.kodelabs.mycosts.domain.interactors; import com.kodelabs.mycosts.domain.executor.Executor; import com.kodelabs.mycosts.domain.executor.MainThread; import com.kodelabs.mycosts.domain.interactors.impl.GetCostByIdInteractorImpl; import com.kodelabs.mycosts.domain.model.Cost; import com.kodelabs.mycosts.domain.repository.CostRepository; import com.kodelabs.mycosts.threading.TestMainThread; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.Date; import static org.mockito.Mockito.when; /** * Created by dmilicic on 1/8/16. */ public class GetCostByIdTest { private MainThread mMainThread; @Mock private Executor mExecutor; @Mock private CostRepository mCostRepository; @Mock private GetCostByIdInteractor.Callback mMockedCallback; private long mCostId; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); mMainThread = new TestMainThread(); mCostId = 100; // any number will do as cost ID } @Test public void testCostNotFound() throws Exception { GetCostByIdInteractorImpl interactor = new GetCostByIdInteractorImpl(mExecutor, mMainThread, mCostId, mCostRepository, mMockedCallback); interactor.run(); Mockito.verify(mCostRepository).getCostById(mCostId); Mockito.verifyNoMoreInteractions(mCostRepository); Mockito.verify(mMockedCallback).noCostFound(); } @Test public void testCostFound() throws Exception { Cost dummyCost = new Cost("Category", "description", new Date(), 100.0); when(mCostRepository.getCostById(mCostId)) .thenReturn(dummyCost); GetCostByIdInteractorImpl interactor = new GetCostByIdInteractorImpl(mExecutor, mMainThread, mCostId, mCostRepository, mMockedCallback); interactor.run(); Mockito.verify(mCostRepository).getCostById(mCostId); Mockito.verifyNoMoreInteractions(mCostRepository); Mockito.verify(mMockedCallback).onCostRetrieved(dummyCost); } } ================================================ FILE: app/src/test/java/com/kodelabs/mycosts/presentation/converter/DailyTotalCostConverterTest.java ================================================ package com.kodelabs.mycosts.presentation.converter; import com.kodelabs.mycosts.domain.model.Cost; import com.kodelabs.mycosts.presentation.model.DailyTotalCost; import com.kodelabs.mycosts.util.TestDateUtil; import org.junit.Test; import java.util.ArrayList; import java.util.Calendar; import java.util.List; import static org.junit.Assert.assertEquals; /** * Created by dmilicic on 1/5/16. */ public class DailyTotalCostConverterTest { private static List mCosts; @Test public void testDailyCostConversion() throws Exception { // init test mCosts = new ArrayList<>(); mCosts.add(new Cost("Transportation", "ZET", TestDateUtil.getDate(2016, Calendar.JANUARY, 4), 100.0)); mCosts.add(new Cost("Groceries", "ZET", TestDateUtil.getDate(2016, Calendar.JANUARY, 4), 200.0)); mCosts.add(new Cost("Entertainment", "ZET", TestDateUtil.getDate(2016, Calendar.JANUARY, 4), 300.0)); mCosts.add(new Cost("Bills", "HEP struja", TestDateUtil.getDate(2016, Calendar.JANUARY, 4), 400.0)); mCosts.add(new Cost("Transportation", "Description", TestDateUtil.getDate(2016, Calendar.JANUARY, 2), 150.0)); mCosts.add(new Cost("Transportation", "Description", TestDateUtil.getDate(2016, Calendar.JANUARY, 2), 110.0)); mCosts.add(new Cost("Transportation", "Description", TestDateUtil.getDate(2016, Calendar.JANUARY, 2), 240.0)); mCosts.add(new Cost("Transportation", "Description", TestDateUtil.getDate(2016, Calendar.JANUARY, 1), 130.0)); mCosts.add(new Cost("Transportation", "Description", TestDateUtil.getDate(2016, Calendar.JANUARY, 1), 230.0)); List dailyTotalCosts = DailyTotalCostConverter.convertCostsToDailyCosts(mCosts); // there should be 3 daily cost objects created for 3 different days assertEquals(3, dailyTotalCosts.size()); // first day should have 4 cost items and a total sum of 1000 assertEquals(4, dailyTotalCosts.get(0).getCostList().size()); assertEquals(1000.0, dailyTotalCosts.get(0).getTotalCost(), 0.00001); // second day should have 3 cost items and a total sum of 500 assertEquals(3, dailyTotalCosts.get(1).getCostList().size()); assertEquals(500.0, dailyTotalCosts.get(1).getTotalCost(), 0.00001); // third day should have 2 cost items and a total sum of 360 assertEquals(2, dailyTotalCosts.get(2).getCostList().size()); assertEquals(360.0, dailyTotalCosts.get(2).getTotalCost(), 0.00001); } @Test public void testDailyCostConversion2() throws Exception { // init test mCosts = new ArrayList<>(); mCosts.add(new Cost("Transportation", "ZET", TestDateUtil.getDate(2016, Calendar.JANUARY, 4), 100.0)); mCosts.add(new Cost("Transportation", "Description", TestDateUtil.getDate(2016, Calendar.JANUARY, 2), 150.0)); mCosts.add(new Cost("Transportation", "Description", TestDateUtil.getDate(2016, Calendar.JANUARY, 2), 110.0)); mCosts.add(new Cost("Transportation", "Description", TestDateUtil.getDate(2016, Calendar.JANUARY, 2), 240.0)); mCosts.add(new Cost("Transportation", "Description", TestDateUtil.getDate(2016, Calendar.JANUARY, 1), 130.0)); mCosts.add(new Cost("Transportation", "Description", TestDateUtil.getDate(2016, Calendar.JANUARY, 1), 230.0)); List dailyTotalCosts = DailyTotalCostConverter.convertCostsToDailyCosts(mCosts); // there should be 3 daily cost objects created for 3 different days assertEquals(3, dailyTotalCosts.size()); assertEquals(1, dailyTotalCosts.get(0).getCostList().size()); assertEquals(100.0, dailyTotalCosts.get(0).getTotalCost(), 0.00001); assertEquals(3, dailyTotalCosts.get(1).getCostList().size()); assertEquals(500.0, dailyTotalCosts.get(1).getTotalCost(), 0.00001); // third day should have 2 cost items and a total sum of 360 assertEquals(2, dailyTotalCosts.get(2).getCostList().size()); assertEquals(360.0, dailyTotalCosts.get(2).getTotalCost(), 0.00001); } } ================================================ FILE: app/src/test/java/com/kodelabs/mycosts/threading/TestMainThread.java ================================================ package com.kodelabs.mycosts.threading; import com.kodelabs.mycosts.domain.executor.MainThread; /** * Created by dmilicic on 1/8/16. */ public class TestMainThread implements MainThread { @Override public void post(Runnable runnable) { // tests can run on this thread, no need to invoke other threads runnable.run(); } } ================================================ FILE: app/src/test/java/com/kodelabs/mycosts/util/TestDateUtil.java ================================================ package com.kodelabs.mycosts.util; import java.util.Calendar; import java.util.Date; /** * Created by dmilicic on 1/9/16. */ public class TestDateUtil { public static Date getDate(int year, int month, int day) { Calendar calendar = Calendar.getInstance(); // set the calendar to start of today calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); calendar.set(year, month, day); return calendar.getTime(); } } ================================================ FILE: build.gradle ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:1.5.0' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { jcenter() maven { url "https://jitpack.io" } } } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Wed Oct 21 11:34:03 PDT 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip ================================================ FILE: gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx10248m -XX:MaxPermSize=256m # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true ================================================ FILE: gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: settings.gradle ================================================ include ':app'