================================================
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
================================================
16dp16dp16dp
================================================
FILE: app/src/main/res/values/strings.xml
================================================
My CostsAboutAdd new costToday\'sYesterday\'s%1$s total costs%1$s costMade by Dario Milicicwww.kodelabs.coAdd costAboutcom.kodelabs.mycosts.accountcom.kodelabs.mycosts.providerEntertainmentEquipmentGiftsGroceriesInsuranceMedicalPaymentRentSalaryShoppingTicketsTransferTransportationUtilitiesBillsOther
================================================
FILE: app/src/main/res/values/styles.xml
================================================
================================================
FILE: app/src/main/res/values-v21/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'