Showing preview only (200K chars total). Download the full file or copy to clipboard to get everything.
Repository: dmilicic/android-clean-sample-app
Branch: master
Commit: 7dcc3aa037c7
Files: 111
Total size: 167.7 KB
Directory structure:
gitextract_jhfjo9q6/
├── .gitignore
├── .idea/
│ ├── .name
│ ├── codeStyleSettings.xml
│ ├── compiler.xml
│ ├── copyright/
│ │ └── profiles_settings.xml
│ ├── gradle.xml
│ ├── misc.xml
│ ├── modules.xml
│ ├── runConfigurations.xml
│ └── vcs.xml
├── LICENSE
├── QA/
│ ├── findbugs/
│ │ └── findbugs-filter.xml
│ └── quality.gradle
├── README.md
├── app/
│ ├── .gitignore
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── com/
│ │ └── kodelabs/
│ │ └── mycosts/
│ │ └── ApplicationTest.java
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── kodelabs/
│ │ │ └── mycosts/
│ │ │ ├── AndroidApplication.java
│ │ │ ├── domain/
│ │ │ │ ├── executor/
│ │ │ │ │ ├── Executor.java
│ │ │ │ │ ├── MainThread.java
│ │ │ │ │ └── impl/
│ │ │ │ │ └── ThreadExecutor.java
│ │ │ │ ├── interactors/
│ │ │ │ │ ├── AddCostInteractor.java
│ │ │ │ │ ├── DeleteCostInteractor.java
│ │ │ │ │ ├── EditCostInteractor.java
│ │ │ │ │ ├── GetAllCostsInteractor.java
│ │ │ │ │ ├── GetCostByIdInteractor.java
│ │ │ │ │ ├── base/
│ │ │ │ │ │ ├── AbstractInteractor.java
│ │ │ │ │ │ └── Interactor.java
│ │ │ │ │ └── impl/
│ │ │ │ │ ├── AddCostInteractorImpl.java
│ │ │ │ │ ├── DeleteCostInteractorImpl.java
│ │ │ │ │ ├── EditCostInteractorImpl.java
│ │ │ │ │ ├── GetAllCostsInteractorImpl.java
│ │ │ │ │ └── GetCostByIdInteractorImpl.java
│ │ │ │ ├── model/
│ │ │ │ │ └── Cost.java
│ │ │ │ └── repository/
│ │ │ │ └── CostRepository.java
│ │ │ ├── network/
│ │ │ │ ├── RestClient.java
│ │ │ │ ├── converters/
│ │ │ │ │ └── RESTModelConverter.java
│ │ │ │ ├── model/
│ │ │ │ │ ├── Payload.java
│ │ │ │ │ └── RESTCost.java
│ │ │ │ └── services/
│ │ │ │ └── SyncService.java
│ │ │ ├── presentation/
│ │ │ │ ├── animation/
│ │ │ │ │ └── AnimatorFactory.java
│ │ │ │ ├── converter/
│ │ │ │ │ └── DailyTotalCostConverter.java
│ │ │ │ ├── model/
│ │ │ │ │ └── DailyTotalCost.java
│ │ │ │ ├── presenters/
│ │ │ │ │ ├── AbstractPresenter.java
│ │ │ │ │ ├── AddCostPresenter.java
│ │ │ │ │ ├── BasePresenter.java
│ │ │ │ │ ├── EditCostPresenter.java
│ │ │ │ │ ├── MainPresenter.java
│ │ │ │ │ └── impl/
│ │ │ │ │ ├── AddCostPresenterImpl.java
│ │ │ │ │ ├── EditCostPresenterImpl.java
│ │ │ │ │ └── MainPresenterImpl.java
│ │ │ │ └── ui/
│ │ │ │ ├── BaseView.java
│ │ │ │ ├── activities/
│ │ │ │ │ ├── AboutActivity.java
│ │ │ │ │ ├── AbstractCostActivity.java
│ │ │ │ │ ├── AddCostActivity.java
│ │ │ │ │ ├── EditCostActivity.java
│ │ │ │ │ └── MainActivity.java
│ │ │ │ ├── adapters/
│ │ │ │ │ └── CostItemAdapter.java
│ │ │ │ ├── customviews/
│ │ │ │ │ ├── CostItemView.java
│ │ │ │ │ └── ExpandedCostView.java
│ │ │ │ ├── fragments/
│ │ │ │ │ └── DatePickerFragment.java
│ │ │ │ └── listeners/
│ │ │ │ ├── IndividualCostViewClickListener.java
│ │ │ │ └── RecyclerViewClickListener.java
│ │ │ ├── storage/
│ │ │ │ ├── CostRepositoryImpl.java
│ │ │ │ ├── contentprovider/
│ │ │ │ │ └── StubProvider.java
│ │ │ │ ├── converters/
│ │ │ │ │ └── StorageModelConverter.java
│ │ │ │ ├── database/
│ │ │ │ │ └── CostDatabase.java
│ │ │ │ └── model/
│ │ │ │ └── Cost.java
│ │ │ ├── sync/
│ │ │ │ ├── SyncAdapter.java
│ │ │ │ ├── SyncService.java
│ │ │ │ └── auth/
│ │ │ │ ├── Authenticator.java
│ │ │ │ ├── AuthenticatorService.java
│ │ │ │ └── DummyAccountProvider.java
│ │ │ ├── threading/
│ │ │ │ └── MainThreadImpl.java
│ │ │ └── utils/
│ │ │ ├── AuthUtils.java
│ │ │ └── DateUtils.java
│ │ └── res/
│ │ ├── anim/
│ │ │ └── hold.xml
│ │ ├── drawable/
│ │ │ └── rounded_corner.xml
│ │ ├── layout/
│ │ │ ├── activity_about.xml
│ │ │ ├── activity_add_cost.xml
│ │ │ ├── activity_main.xml
│ │ │ ├── card_daily_cost_item.xml
│ │ │ ├── card_expanded_daily_cost_item.xml
│ │ │ ├── content_about.xml
│ │ │ ├── content_add_cost.xml
│ │ │ ├── expanded_cost_item.xml
│ │ │ └── individual_cost_item.xml
│ │ ├── menu/
│ │ │ ├── menu_add_cost.xml
│ │ │ ├── menu_cost_item.xml
│ │ │ └── menu_main.xml
│ │ ├── values/
│ │ │ ├── colors.xml
│ │ │ ├── dimens.xml
│ │ │ ├── strings.xml
│ │ │ └── styles.xml
│ │ ├── values-v21/
│ │ │ └── styles.xml
│ │ ├── values-w820dp/
│ │ │ └── dimens.xml
│ │ └── xml/
│ │ ├── authenticator.xml
│ │ └── syncadapter.xml
│ └── test/
│ └── java/
│ └── com/
│ └── kodelabs/
│ └── mycosts/
│ ├── domain/
│ │ └── interactors/
│ │ └── GetCostByIdTest.java
│ ├── presentation/
│ │ └── converter/
│ │ └── DailyTotalCostConverterTest.java
│ ├── threading/
│ │ └── TestMainThread.java
│ └── util/
│ └── TestDateUtil.java
├── build.gradle
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
=======
# Built application files
*.apk
*.ap_
# Files for the Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
# Gradle files
.gradle/
build/
/*/build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
================================================
FILE: .idea/.name
================================================
My Costs
================================================
FILE: .idea/codeStyleSettings.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectCodeStyleSettingsManager">
<option name="PER_PROJECT_SETTINGS">
<value>
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
<value />
</option>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
</value>
</option>
<option name="RIGHT_MARGIN" value="100" />
<AndroidXmlCodeStyleSettings>
<option name="USE_CUSTOM_SETTINGS" value="true" />
</AndroidXmlCodeStyleSettings>
<Objective-C-extensions>
<option name="GENERATE_INSTANCE_VARIABLES_FOR_PROPERTIES" value="ASK" />
<option name="RELEASE_STYLE" value="IVAR" />
<option name="TYPE_QUALIFIERS_PLACEMENT" value="BEFORE" />
<file>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
</file>
<class>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
</class>
<extensions>
<pair source="cpp" header="h" />
<pair source="c" header="h" />
</extensions>
</Objective-C-extensions>
<XML>
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
</XML>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_NAMESPACE>Namespace:</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_NAMESPACE>Namespace:</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_width</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_height</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:width</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:height</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</value>
</option>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Dario's" />
</component>
</project>
================================================
FILE: .idea/compiler.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<option name="DEFAULT_COMPILER" value="Javac" />
<resourceExtensions />
<wildcardResourcePatterns>
<entry name="!?*.java" />
<entry name="!?*.form" />
<entry name="!?*.class" />
<entry name="!?*.groovy" />
<entry name="!?*.scala" />
<entry name="!?*.flex" />
<entry name="!?*.kt" />
<entry name="!?*.clj" />
</wildcardResourcePatterns>
<annotationProcessing>
<profile default="true" name="Default" enabled="false">
<processorPath useClasspath="true" />
</profile>
</annotationProcessing>
</component>
</project>
================================================
FILE: .idea/copyright/profiles_settings.xml
================================================
<component name="CopyrightManager">
<settings default="" />
</component>
================================================
FILE: .idea/gradle.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="distributionType" value="LOCAL" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="$APPLICATION_HOME_DIR$/gradle/gradle-2.8" />
<option name="gradleJvm" value="1.7" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>
================================================
FILE: .idea/misc.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EntryPointsManager">
<entry_points version="2.0" />
</component>
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectLevelVcsManager" settingsEditedManually="false">
<OptionsSetting value="true" id="Add" />
<OptionsSetting value="true" id="Remove" />
<OptionsSetting value="true" id="Checkout" />
<OptionsSetting value="true" id="Update" />
<OptionsSetting value="true" id="Status" />
<OptionsSetting value="true" id="Edit" />
<ConfirmationsSetting value="0" id="Add" />
<ConfirmationsSetting value="0" id="Remove" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" assert-keyword="true" jdk-15="true" project-jdk-name="1.7" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
<component name="masterDetails">
<states>
<state key="GlobalLibrariesConfigurable.UI">
<settings>
<splitter-proportions>
<option name="proportions">
<list>
<option value="0.2" />
</list>
</option>
</splitter-proportions>
</settings>
</state>
<state key="JdkListConfigurable.UI">
<settings>
<last-edited>Android SDK</last-edited>
<splitter-proportions>
<option name="proportions">
<list>
<option value="0.2" />
</list>
</option>
</splitter-proportions>
</settings>
</state>
<state key="ProjectJDKs.UI">
<settings>
<last-edited>Android API 21 Platform</last-edited>
<splitter-proportions>
<option name="proportions">
<list>
<option value="0.2" />
</list>
</option>
</splitter-proportions>
</settings>
</state>
<state key="ProjectLibrariesConfigurable.UI">
<settings>
<splitter-proportions>
<option name="proportions">
<list>
<option value="0.2" />
</list>
</option>
</splitter-proportions>
</settings>
</state>
<state key="ScopeChooserConfigurable.UI">
<settings>
<splitter-proportions>
<option name="proportions">
<list>
<option value="0.2" />
</list>
</option>
</splitter-proportions>
</settings>
</state>
</states>
</component>
</project>
================================================
FILE: .idea/modules.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/MyCosts.iml" filepath="$PROJECT_DIR$/MyCosts.iml" />
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
</modules>
</component>
</project>
================================================
FILE: .idea/runConfigurations.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>
================================================
FILE: .idea/vcs.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
================================================
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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<FindBugsFilter>
<Match>
<!-- ignore all issues in resource generation -->
<Class name="~.*\.R\$.*" />
</Match>
<Match>
<Class name="~.*\.Manifest\$.*" />
</Match>
<!-- ignore all bugs in test classes, except for those bugs specifically relating to JUnit tests -->
<Match>
<Class name="~.*\.*Test" />
<!-- test classes are suffixed by 'Test' -->
<Not>
<Bug code="IJU" />
<!-- 'IJU' is the code for bugs related to JUnit test code -->
</Not>
</Match>
</FindBugsFilter>
================================================
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]: <https://github.com/dmilicic/mycosts-rails-backend>
[detailed guide]: <https://medium.com/@dmilicic/a-detailed-guide-on-developing-android-apps-using-the-clean-architecture-pattern-d38d71e94029>
[blog]: <https://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html>
[MVP]: <https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter>
[MPAndroidChart]: <https://github.com/PhilJay/MPAndroidChart>
[DBFlow]: <https://github.com/Raizlabs/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;
/**
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
*/
public class ApplicationTest extends ApplicationTestCase<Application> {
public ApplicationTest() {
super(Application.class);
}
}
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest
package="com.kodelabs.mycosts"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<application
android:name=".AndroidApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<activity
android:name=".presentation.ui.activities.MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".presentation.ui.activities.AddCostActivity"
android:label="Add cost"
android:theme="@style/AppTheme" />
<activity
android:name=".presentation.ui.activities.EditCostActivity"
android:label="Edit cost"
android:theme="@style/AppTheme" />
<activity
android:name=".presentation.ui.activities.AboutActivity"
android:label="@string/title_activity_about"
android:parentActivityName=".presentation.ui.activities.MainActivity"
android:theme="@style/AppTheme">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.kodelabs.mycosts.presentation.ui.activities.MainActivity" />
</activity>
<!-- Sync service section -->
<service
android:name=".sync.auth.AuthenticatorService"
android:exported="false">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
<service
android:name=".sync.SyncService"
android:exported="false"
android:process=":sync">
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/syncadapter" />
</service>
<provider
android:name=".storage.contentprovider.StubProvider"
android:authorities="@string/stub_content_authority"
android:exported="false"
android:syncable="true" />
</application>
</manifest>
================================================
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.
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* 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<Runnable> WORK_QUEUE = new LinkedBlockingQueue<Runnable>();
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.
* <p/>
* This interactor is responsible for retrieving a list of costs from the database.
*/
public interface GetAllCostsInteractor extends Interactor {
interface Callback {
void onCostsRetrieved(List<Cost> 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.
* <p/>
* 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).
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* Created by dmilicic on 12/10/15.
*/
public class GetAllCostsInteractorImpl extends AbstractInteractor implements GetAllCostsInteractor {
private Callback mCallback;
private CostRepository mCostRepository;
private Comparator<Cost> mCostComparator = new Comparator<Cost>() {
@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<Cost> 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.
* <p/>
* 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<Cost> getAllCosts();
List<Cost> getAllUnsyncedCosts();
void markSynced(List<Cost> 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;
/**
* <p/>
* 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> T getService(Class<T> 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<RESTCost> mCosts;
public Payload(String username) {
mUsername = username;
mCosts = new ArrayList<>();
}
public String getUsername() {
return mUsername;
}
public List<RESTCost> 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.
* <p/>
*/
public interface SyncService {
/**
* This endpoint will be used to send new costs created on this device.
*/
@Headers("Connection: close")
@POST("/costs")
Call<Void> 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<DailyTotalCost> convertCostsToDailyCosts(List<Cost> costList) {
List<DailyTotalCost> 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<Cost> 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<Cost> mCostList;
private Date mDate;
private double mTotalCost;
public DailyTotalCost(List<Cost> 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<Cost> 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<DailyTotalCost> 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<Cost> costList) {
List<DailyTotalCost> 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.
* <p/>
* 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<DailyTotalCost> 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<RecyclerView.ViewHolder> implements RecyclerViewClickListener {
private enum ViewType {
CONTRACTED_CARD, EXPANDED_CARD
}
private List<DailyTotalCost> mCostList;
private Context mContext;
private Set<Integer> 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<DailyTotalCost> 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<Cost> 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<com.kodelabs.mycosts.storage.model.Cost> 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<Cost> getAllCosts() {
List<com.kodelabs.mycosts.storage.model.Cost> costs = SQLite
.select()
.from(com.kodelabs.mycosts.storage.model.Cost.class)
.queryList();
return StorageModelConverter.convertListToDomainModel(costs);
}
@Override
public List<Cost> getAllUnsyncedCosts() {
List<com.kodelabs.mycosts.storage.model.Cost> 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<Cost> costs) {
// we have to convert it to the database model before storing
List<com.kodelabs.mycosts.storage.model.Cost> 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<com.kodelabs.mycosts.domain.model.Cost> convertListToDomainModel(List<Cost> costs) {
List<com.kodelabs.mycosts.domain.model.Cost> convertedCosts = new ArrayList<>();
for (Cost cost : costs) {
convertedCosts.add(convertToDomainModel(cost));
}
// cleanup
costs.clear();
costs = null;
return convertedCosts;
}
public static List<Cost> convertListToStorageModel(List<com.kodelabs.mycosts.domain.model.Cost> costs) {
List<Cost> 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<Cost> 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<Void> mResponseCallback = new Callback<Void>() {
@Override
public void onResponse(Call<Void> call, Response<Void> response) {
Timber.i("UPLOAD SUCCESS: %d", response.code());
if (response.isSuccess()) {
mCostRepository.markSynced(mUnsyncedCosts);
}
}
@Override
public void onFailure(Call<Void> 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<Void> 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
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* 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
================================================
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<translate
android:fromXDelta="0%"
android:fromYDelta="0%"
android:toXDelta="0%"
android:toYDelta="0%" />
</set>
================================================
FILE: app/src/main/res/drawable/rounded_corner.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/colorPrimary" />
<corners
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"
android:topLeftRadius="3dp"
android:topRightRadius="3dp" />
<padding
android:bottom="0dip"
android:left="0dip"
android:right="0dip"
android:top="0dip" />
</shape>
================================================
FILE: app/src/main/res/layout/activity_about.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="com.kodelabs.mycosts.presentation.ui.activities.AboutActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<include layout="@layout/content_about" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email" />
</android.support.design.widget.CoordinatorLayout>
================================================
FILE: app/src/main/res/layout/activity_add_cost.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="com.kodelabs.mycosts.presentation.ui.activities.AddCostActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<include
layout="@layout/content_add_cost"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?attr/actionBarSize" />
</FrameLayout>
================================================
FILE: app/src/main/res/layout/activity_main.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<io.codetail.widget.RevealFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".presentation.ui.activities.MainActivity">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/AppTheme.AppBarOverlay" />
<android.support.v7.widget.RecyclerView
android:id="@+id/expenses_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?attr/actionBarSize">
</android.support.v7.widget.RecyclerView>
<!-- This layout will be revealed when FAB is clicked -->
<include
layout="@layout/content_add_cost"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?attr/actionBarSize" />
</io.codetail.widget.RevealFrameLayout>
================================================
FILE: app/src/main/res/layout/card_daily_cost_item.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
android:id="@+id/cost_item_card"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="124dp"
android:layout_marginBottom="10dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginTop="20dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/cost_item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:text="Today's total expenses"
android:textSize="15sp" />
<TextView
android:id="@+id/cost_item_total_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="16dp"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:text="127$"
android:textSize="40sp" />
</RelativeLayout>
</android.support.v7.widget.CardView>
================================================
FILE: app/src/main/res/layout/card_expanded_daily_cost_item.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<com.kodelabs.mycosts.presentation.ui.customviews.ExpandedCostView
android:id="@+id/card_expanded_costview"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginTop="20dp"
app:cardPreventCornerOverlap="false" />
================================================
FILE: app/src/main/res/layout/content_about.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="com.kodelabs.mycosts.presentation.ui.activities.AboutActivity"
tools:showIn="@layout/activity_about">
<TextView
android:id="@+id/made_by_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="@string/made_by"
android:textSize="20sp" />
<TextView
android:id="@+id/website_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/made_by_textview"
android:layout_centerHorizontal="true"
android:autoLink="web"
android:text="@string/kodelabs_website"
android:textSize="20sp"
android:textStyle="bold" />
</RelativeLayout>
================================================
FILE: app/src/main/res/layout/content_add_cost.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<io.codetail.widget.RevealFrameLayout
android:id="@+id/reveal_layout"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#fff"
android:visibility="visible">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_column="0"
android:layout_gravity="center"
android:layout_margin="16dp"
android:src="@drawable/ic_date_range_black_24dp" />
<TextView
android:id="@+id/input_date"
style="@android:style/Widget.DeviceDefault.Light.Spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="Enter date"
android:padding="20dp" />
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/input_amount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="0.00"
android:inputType="numberDecimal"
android:padding="16dp" />
<TextView
android:id="@+id/textview_amount"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:ems="10"
android:hint="USD"
android:padding="20dp" />
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/textview_category"
android:layout_width="110dp"
android:layout_height="wrap_content"
android:ems="10"
android:hint="Category:"
android:padding="20dp" />
<Spinner
android:id="@+id/input_cost_category"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:entries="@array/category_array"
android:padding="16dp" />
</LinearLayout>
<EditText
android:id="@+id/input_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="Enter description"
android:inputType="text"
android:padding="16dp" />
</LinearLayout>
</io.codetail.widget.RevealFrameLayout>
================================================
FILE: app/src/main/res/layout/expanded_cost_item.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="130dp"
android:layout_marginBottom="10dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginTop="20dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="65dp"
android:background="@drawable/rounded_corner">
<TextView
android:id="@+id/cost_item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_margin="20dp"
android:text="Today's total expenses"
android:textColor="#fff"
android:textSize="17sp" />
<TextView
android:id="@+id/cost_item_total_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginEnd="20dp"
android:layout_marginRight="20dp"
android:paddingTop="5dp"
android:text="127$"
android:textColor="#fff"
android:textSize="17sp" />
</RelativeLayout>
<LinearLayout
android:id="@+id/layout_individual_cost_items"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/cost_item_total_value"
android:orientation="vertical">
</LinearLayout>
</LinearLayout>
</merge>
================================================
FILE: app/src/main/res/layout/individual_cost_item.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="16dp">
<TextView
android:id="@+id/cost_item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="Groceries"
android:textSize="15sp" />
<TextView
android:id="@+id/cost_item_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/cost_item_title"
android:layout_margin="16dp"
android:text="This description describes a describable item"
android:textSize="12sp" />
<ImageButton
android:id="@+id/button_menu"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp"
android:background="@null"
android:src="@drawable/ic_vertical_dots" />
<TextView
android:id="@+id/cost_item_total_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@id/cost_item_title"
android:layout_marginEnd="25dp"
android:layout_marginRight="25dp"
android:layout_toLeftOf="@id/button_menu"
android:layout_toStartOf="@id/button_menu"
android:text="127$"
android:textSize="15sp" />
</RelativeLayout>
================================================
FILE: app/src/main/res/menu/menu_add_cost.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_save"
android:orderInCategory="1"
android:title="Save"
app:showAsAction="always" />
</menu>
================================================
FILE: app/src/main/res/menu/menu_cost_item.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/item_edit"
android:title="Edit"
android:visible="true"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/item_delete"
android:title="Delete"
android:visible="true"
app:showAsAction="ifRoom|withText" />
</menu>
================================================
FILE: app/src/main/res/menu/menu_main.xml
================================================
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".presentation.ui.activities.MainActivity">
<item
android:id="@+id/action_add_cost"
android:actionViewClass="android.widget.ImageButton"
android:icon="@drawable/ic_add_white_24dp"
android:orderInCategory="1"
android:title="@string/action_add_cost"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_about"
android:orderInCategory="100"
android:title="@string/action_about"
app:showAsAction="never" />
</menu>
================================================
FILE: app/src/main/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<color name="colorBrightGray">#f1f1f1</color>
</resources>
================================================
FILE: app/src/main/res/values/dimens.xml
================================================
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="fab_margin">16dp</dimen>
</resources>
================================================
FILE: app/src/main/res/values/strings.xml
================================================
<resources>
<string name="app_name">My Costs</string>
<!-- Actions -->
<string name="action_about">About</string>
<string name="action_add_cost">Add new cost</string>
<!-- General -->
<string name="today_s">Today\'s</string>
<string name="yesterday_s">Yesterday\'s</string>
<string name="total_expenses">%1$s total costs</string>
<string name="cost">%1$s cost</string>
<!-- About -->
<string name="made_by">Made by Dario Milicic</string>
<string name="kodelabs_website">www.kodelabs.co</string>
<!-- Titles -->
<string name="title_activity_add_cost">Add cost</string>
<string name="title_activity_about">About</string>
<string name="account_type">com.kodelabs.mycosts.account</string>
<string name="stub_content_authority">com.kodelabs.mycosts.provider</string>
<string-array name="category_array">
<item>Entertainment</item>
<item>Equipment</item>
<item>Gifts</item>
<item>Groceries</item>
<item>Insurance</item>
<item>Medical</item>
<item>Payment</item>
<item>Rent</item>
<item>Salary</item>
<item>Shopping</item>
<item>Tickets</item>
<item>Transfer</item>
<item>Transportation</item>
<item>Utilities</item>
<item>Bills</item>
<item>Other</item>
</string-array>
</resources>
================================================
FILE: app/src/main/res/values/styles.xml
================================================
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>
================================================
FILE: app/src/main/res/values-v21/styles.xml
================================================
<resources>>
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>
================================================
FILE: app/src/main/res/values-w820dp/dimens.xml
================================================
<resources>
<!-- Example customization of dimensions originally defined in res/values/dimens.xml
(such as screen margins) for screens with more than 820dp of available width. This
would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
<dimen name="activity_horizontal_margin">64dp</dimen>
</resources>
================================================
FILE: app/src/main/res/xml/authenticator.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="@string/account_type"
android:icon="@android:drawable/ic_dialog_info"
android:label="@string/app_name"
android:smallIcon="@android:drawable/ic_dialog_info" />
================================================
FILE: app/src/main/res/xml/syncadapter.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="@string/account_type"
android:allowParallelSyncs="false"
android:contentAuthority="@string/stub_content_authority"
android:isAlwaysSyncable="true"
android:supportsUploading="true"
android:userVisible="false" />
================================================
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<Cost> 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<DailyTotalCost> 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<DailyTotalCost> 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'
gitextract_jhfjo9q6/ ├── .gitignore ├── .idea/ │ ├── .name │ ├── codeStyleSettings.xml │ ├── compiler.xml │ ├── copyright/ │ │ └── profiles_settings.xml │ ├── gradle.xml │ ├── misc.xml │ ├── modules.xml │ ├── runConfigurations.xml │ └── vcs.xml ├── LICENSE ├── QA/ │ ├── findbugs/ │ │ └── findbugs-filter.xml │ └── quality.gradle ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── kodelabs/ │ │ └── mycosts/ │ │ └── ApplicationTest.java │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── kodelabs/ │ │ │ └── mycosts/ │ │ │ ├── AndroidApplication.java │ │ │ ├── domain/ │ │ │ │ ├── executor/ │ │ │ │ │ ├── Executor.java │ │ │ │ │ ├── MainThread.java │ │ │ │ │ └── impl/ │ │ │ │ │ └── ThreadExecutor.java │ │ │ │ ├── interactors/ │ │ │ │ │ ├── AddCostInteractor.java │ │ │ │ │ ├── DeleteCostInteractor.java │ │ │ │ │ ├── EditCostInteractor.java │ │ │ │ │ ├── GetAllCostsInteractor.java │ │ │ │ │ ├── GetCostByIdInteractor.java │ │ │ │ │ ├── base/ │ │ │ │ │ │ ├── AbstractInteractor.java │ │ │ │ │ │ └── Interactor.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── AddCostInteractorImpl.java │ │ │ │ │ ├── DeleteCostInteractorImpl.java │ │ │ │ │ ├── EditCostInteractorImpl.java │ │ │ │ │ ├── GetAllCostsInteractorImpl.java │ │ │ │ │ └── GetCostByIdInteractorImpl.java │ │ │ │ ├── model/ │ │ │ │ │ └── Cost.java │ │ │ │ └── repository/ │ │ │ │ └── CostRepository.java │ │ │ ├── network/ │ │ │ │ ├── RestClient.java │ │ │ │ ├── converters/ │ │ │ │ │ └── RESTModelConverter.java │ │ │ │ ├── model/ │ │ │ │ │ ├── Payload.java │ │ │ │ │ └── RESTCost.java │ │ │ │ └── services/ │ │ │ │ └── SyncService.java │ │ │ ├── presentation/ │ │ │ │ ├── animation/ │ │ │ │ │ └── AnimatorFactory.java │ │ │ │ ├── converter/ │ │ │ │ │ └── DailyTotalCostConverter.java │ │ │ │ ├── model/ │ │ │ │ │ └── DailyTotalCost.java │ │ │ │ ├── presenters/ │ │ │ │ │ ├── AbstractPresenter.java │ │ │ │ │ ├── AddCostPresenter.java │ │ │ │ │ ├── BasePresenter.java │ │ │ │ │ ├── EditCostPresenter.java │ │ │ │ │ ├── MainPresenter.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── AddCostPresenterImpl.java │ │ │ │ │ ├── EditCostPresenterImpl.java │ │ │ │ │ └── MainPresenterImpl.java │ │ │ │ └── ui/ │ │ │ │ ├── BaseView.java │ │ │ │ ├── activities/ │ │ │ │ │ ├── AboutActivity.java │ │ │ │ │ ├── AbstractCostActivity.java │ │ │ │ │ ├── AddCostActivity.java │ │ │ │ │ ├── EditCostActivity.java │ │ │ │ │ └── MainActivity.java │ │ │ │ ├── adapters/ │ │ │ │ │ └── CostItemAdapter.java │ │ │ │ ├── customviews/ │ │ │ │ │ ├── CostItemView.java │ │ │ │ │ └── ExpandedCostView.java │ │ │ │ ├── fragments/ │ │ │ │ │ └── DatePickerFragment.java │ │ │ │ └── listeners/ │ │ │ │ ├── IndividualCostViewClickListener.java │ │ │ │ └── RecyclerViewClickListener.java │ │ │ ├── storage/ │ │ │ │ ├── CostRepositoryImpl.java │ │ │ │ ├── contentprovider/ │ │ │ │ │ └── StubProvider.java │ │ │ │ ├── converters/ │ │ │ │ │ └── StorageModelConverter.java │ │ │ │ ├── database/ │ │ │ │ │ └── CostDatabase.java │ │ │ │ └── model/ │ │ │ │ └── Cost.java │ │ │ ├── sync/ │ │ │ │ ├── SyncAdapter.java │ │ │ │ ├── SyncService.java │ │ │ │ └── auth/ │ │ │ │ ├── Authenticator.java │ │ │ │ ├── AuthenticatorService.java │ │ │ │ └── DummyAccountProvider.java │ │ │ ├── threading/ │ │ │ │ └── MainThreadImpl.java │ │ │ └── utils/ │ │ │ ├── AuthUtils.java │ │ │ └── DateUtils.java │ │ └── res/ │ │ ├── anim/ │ │ │ └── hold.xml │ │ ├── drawable/ │ │ │ └── rounded_corner.xml │ │ ├── layout/ │ │ │ ├── activity_about.xml │ │ │ ├── activity_add_cost.xml │ │ │ ├── activity_main.xml │ │ │ ├── card_daily_cost_item.xml │ │ │ ├── card_expanded_daily_cost_item.xml │ │ │ ├── content_about.xml │ │ │ ├── content_add_cost.xml │ │ │ ├── expanded_cost_item.xml │ │ │ └── individual_cost_item.xml │ │ ├── menu/ │ │ │ ├── menu_add_cost.xml │ │ │ ├── menu_cost_item.xml │ │ │ └── menu_main.xml │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ ├── values-v21/ │ │ │ └── styles.xml │ │ ├── values-w820dp/ │ │ │ └── dimens.xml │ │ └── xml/ │ │ ├── authenticator.xml │ │ └── syncadapter.xml │ └── test/ │ └── java/ │ └── com/ │ └── kodelabs/ │ └── mycosts/ │ ├── domain/ │ │ └── interactors/ │ │ └── GetCostByIdTest.java │ ├── presentation/ │ │ └── converter/ │ │ └── DailyTotalCostConverterTest.java │ ├── threading/ │ │ └── TestMainThread.java │ └── util/ │ └── TestDateUtil.java ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle
SYMBOL INDEX (337 symbols across 64 files)
FILE: app/src/androidTest/java/com/kodelabs/mycosts/ApplicationTest.java
class ApplicationTest (line 9) | public class ApplicationTest extends ApplicationTestCase<Application> {
method ApplicationTest (line 10) | public ApplicationTest() {
FILE: app/src/main/java/com/kodelabs/mycosts/AndroidApplication.java
class AndroidApplication (line 14) | public class AndroidApplication extends Application {
method onCreate (line 15) | @Override
FILE: app/src/main/java/com/kodelabs/mycosts/domain/executor/Executor.java
type Executor (line 10) | public interface Executor {
method execute (line 18) | void execute(final AbstractInteractor interactor);
FILE: app/src/main/java/com/kodelabs/mycosts/domain/executor/MainThread.java
type MainThread (line 10) | public interface MainThread {
method post (line 17) | void post(final Runnable runnable);
FILE: app/src/main/java/com/kodelabs/mycosts/domain/executor/impl/ThreadExecutor.java
class ThreadExecutor (line 16) | public class ThreadExecutor implements Executor {
method ThreadExecutor (line 29) | private ThreadExecutor() {
method execute (line 39) | @Override
method getInstance (line 57) | public static Executor getInstance() {
FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/AddCostInteractor.java
type AddCostInteractor (line 8) | public interface AddCostInteractor extends Interactor {
type Callback (line 10) | interface Callback {
method onCostAdded (line 11) | void onCostAdded();
FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/DeleteCostInteractor.java
type DeleteCostInteractor (line 9) | public interface DeleteCostInteractor extends Interactor {
type Callback (line 11) | interface Callback {
method onCostDeleted (line 12) | void onCostDeleted(Cost cost);
FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/EditCostInteractor.java
type EditCostInteractor (line 9) | public interface EditCostInteractor extends Interactor {
type Callback (line 11) | interface Callback {
method onCostUpdated (line 13) | void onCostUpdated(Cost cost);
FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/GetAllCostsInteractor.java
type GetAllCostsInteractor (line 13) | public interface GetAllCostsInteractor extends Interactor {
type Callback (line 15) | interface Callback {
method onCostsRetrieved (line 16) | void onCostsRetrieved(List<Cost> costList);
FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/GetCostByIdInteractor.java
type GetCostByIdInteractor (line 9) | public interface GetCostByIdInteractor extends Interactor {
type Callback (line 11) | interface Callback {
method onCostRetrieved (line 12) | void onCostRetrieved(Cost cost);
method noCostFound (line 14) | void noCostFound();
FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/base/AbstractInteractor.java
class AbstractInteractor (line 16) | public abstract class AbstractInteractor implements Interactor {
method AbstractInteractor (line 24) | public AbstractInteractor(Executor threadExecutor, MainThread mainThre...
method run (line 36) | public abstract void run();
method cancel (line 38) | public void cancel() {
method isRunning (line 43) | public boolean isRunning() {
method onFinished (line 47) | public void onFinished() {
method execute (line 52) | public void execute() {
FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/base/Interactor.java
type Interactor (line 6) | public interface Interactor {
method execute (line 12) | void execute();
FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/impl/AddCostInteractorImpl.java
class AddCostInteractorImpl (line 18) | public class AddCostInteractorImpl extends AbstractInteractor implements...
method AddCostInteractorImpl (line 28) | public AddCostInteractorImpl(Executor threadExecutor, MainThread mainT...
method run (line 40) | @Override
FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/impl/DeleteCostInteractorImpl.java
class DeleteCostInteractorImpl (line 15) | public class DeleteCostInteractorImpl extends AbstractInteractor impleme...
method DeleteCostInteractorImpl (line 21) | public DeleteCostInteractorImpl(Executor threadExecutor,
method run (line 30) | @Override
FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/impl/EditCostInteractorImpl.java
class EditCostInteractorImpl (line 17) | public class EditCostInteractorImpl extends AbstractInteractor implement...
method EditCostInteractorImpl (line 31) | public EditCostInteractorImpl(Executor threadExecutor, MainThread main...
method run (line 44) | @Override
FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/impl/GetAllCostsInteractorImpl.java
class GetAllCostsInteractorImpl (line 20) | public class GetAllCostsInteractorImpl extends AbstractInteractor implem...
method compare (line 26) | @Override
method GetAllCostsInteractorImpl (line 39) | public GetAllCostsInteractorImpl(Executor threadExecutor, MainThread m...
method run (line 51) | @Override
FILE: app/src/main/java/com/kodelabs/mycosts/domain/interactors/impl/GetCostByIdInteractorImpl.java
class GetCostByIdInteractorImpl (line 16) | public class GetCostByIdInteractorImpl extends AbstractInteractor implem...
method GetCostByIdInteractorImpl (line 23) | public GetCostByIdInteractorImpl(Executor threadExecutor, MainThread m...
method run (line 32) | @Override
FILE: app/src/main/java/com/kodelabs/mycosts/domain/model/Cost.java
class Cost (line 8) | public class Cost {
method Cost (line 15) | public Cost(String category, String description, Date date, double amo...
method Cost (line 31) | public Cost(String category, String description, Date date, double amo...
method setCategory (line 39) | public void setCategory(String category) {
method setDescription (line 43) | public void setDescription(String description) {
method setDate (line 47) | public void setDate(Date date) {
method setAmount (line 51) | public void setAmount(double amount) {
method getId (line 55) | public long getId() {
method getCategory (line 59) | public String getCategory() {
method getDescription (line 63) | public String getDescription() {
method getDate (line 67) | public Date getDate() {
method getAmount (line 71) | public double getAmount() {
method equals (line 75) | @Override
method hashCode (line 86) | @Override
method toString (line 91) | @Override
FILE: app/src/main/java/com/kodelabs/mycosts/domain/repository/CostRepository.java
type CostRepository (line 10) | public interface CostRepository {
method insert (line 12) | void insert(Cost cost);
method update (line 14) | void update(Cost cost);
method getCostById (line 16) | Cost getCostById(long id);
method getAllCosts (line 18) | List<Cost> getAllCosts();
method getAllUnsyncedCosts (line 20) | List<Cost> getAllUnsyncedCosts();
method markSynced (line 22) | void markSynced(List<Cost> costs);
method delete (line 24) | void delete(Cost cost);
FILE: app/src/main/java/com/kodelabs/mycosts/network/RestClient.java
class RestClient (line 15) | public class RestClient {
method getService (line 44) | public static <T> T getService(Class<T> serviceClass) {
FILE: app/src/main/java/com/kodelabs/mycosts/network/converters/RESTModelConverter.java
class RESTModelConverter (line 11) | public class RESTModelConverter {
method convertToRestModel (line 13) | public static RESTCost convertToRestModel(Cost cost) {
FILE: app/src/main/java/com/kodelabs/mycosts/network/model/Payload.java
class Payload (line 13) | public class Payload {
method Payload (line 21) | public Payload(String username) {
method getUsername (line 26) | public String getUsername() {
method getCosts (line 30) | public List<RESTCost> getCosts() {
method addCost (line 34) | public void addCost(RESTCost cost) {
method main (line 38) | public static void main(String[] args) {
FILE: app/src/main/java/com/kodelabs/mycosts/network/model/RESTCost.java
class RESTCost (line 10) | public class RESTCost {
method RESTCost (line 28) | public RESTCost(long id, String category, String description, Date dat...
method getId (line 36) | public long getId() {
method getCategory (line 40) | public String getCategory() {
method getDescription (line 44) | public String getDescription() {
method getDate (line 48) | public Date getDate() {
method getAmount (line 52) | public double getAmount() {
FILE: app/src/main/java/com/kodelabs/mycosts/network/services/SyncService.java
type SyncService (line 14) | public interface SyncService {
method uploadData (line 19) | @Headers("Connection: close")
FILE: app/src/main/java/com/kodelabs/mycosts/presentation/animation/AnimatorFactory.java
class AnimatorFactory (line 20) | public class AnimatorFactory {
method enterReveal (line 33) | public static void enterReveal(ViewGroup revealLayout, final Intent in...
FILE: app/src/main/java/com/kodelabs/mycosts/presentation/converter/DailyTotalCostConverter.java
class DailyTotalCostConverter (line 13) | public class DailyTotalCostConverter {
method convertCostsToDailyCosts (line 15) | public static List<DailyTotalCost> convertCostsToDailyCosts(List<Cost>...
FILE: app/src/main/java/com/kodelabs/mycosts/presentation/model/DailyTotalCost.java
class DailyTotalCost (line 11) | public class DailyTotalCost {
method DailyTotalCost (line 18) | public DailyTotalCost(List<Cost> costList, Date date) {
method getCostList (line 29) | public List<Cost> getCostList() {
method getDate (line 33) | public Date getDate() {
method getTotalCost (line 37) | public double getTotalCost() {
method toString (line 41) | @Override
FILE: app/src/main/java/com/kodelabs/mycosts/presentation/presenters/AbstractPresenter.java
class AbstractPresenter (line 9) | public abstract class AbstractPresenter {
method AbstractPresenter (line 13) | public AbstractPresenter(Executor executor, MainThread mainThread) {
FILE: app/src/main/java/com/kodelabs/mycosts/presentation/presenters/AddCostPresenter.java
type AddCostPresenter (line 10) | public interface AddCostPresenter extends BasePresenter {
type View (line 13) | interface View extends BaseView {
method onCostAdded (line 15) | void onCostAdded();
method addNewCost (line 18) | void addNewCost(Date date, double amount, String description, String c...
FILE: app/src/main/java/com/kodelabs/mycosts/presentation/presenters/BasePresenter.java
type BasePresenter (line 6) | public interface BasePresenter {
method resume (line 11) | void resume();
method pause (line 17) | void pause();
method stop (line 23) | void stop();
method destroy (line 29) | void destroy();
method onError (line 35) | void onError(String message);
FILE: app/src/main/java/com/kodelabs/mycosts/presentation/presenters/EditCostPresenter.java
type EditCostPresenter (line 11) | public interface EditCostPresenter {
type View (line 13) | interface View extends BaseView {
method onCostRetrieved (line 15) | void onCostRetrieved(Cost cost);
method onCostUpdated (line 17) | void onCostUpdated(Cost cost);
method getCostById (line 20) | void getCostById(long id);
method editCost (line 22) | void editCost(Cost cost, Date date, double amount, String description,...
FILE: app/src/main/java/com/kodelabs/mycosts/presentation/presenters/MainPresenter.java
type MainPresenter (line 12) | public interface MainPresenter extends BasePresenter {
type View (line 14) | interface View extends BaseView {
method showCosts (line 16) | void showCosts(List<DailyTotalCost> costs);
method onClickDeleteCost (line 18) | void onClickDeleteCost(long costId);
method onClickEditCost (line 20) | void onClickEditCost(long costId, int position);
method onCostDeleted (line 22) | void onCostDeleted(Cost cost);
method getAllCosts (line 25) | void getAllCosts();
method deleteCost (line 27) | void deleteCost(long costId);
FILE: app/src/main/java/com/kodelabs/mycosts/presentation/presenters/impl/AddCostPresenterImpl.java
class AddCostPresenterImpl (line 16) | public class AddCostPresenterImpl extends AbstractPresenter implements A...
method AddCostPresenterImpl (line 22) | public AddCostPresenterImpl(Executor executor, MainThread mainThread,
method addNewCost (line 29) | @Override
method onCostAdded (line 42) | @Override
method resume (line 48) | @Override
method pause (line 53) | @Override
method stop (line 58) | @Override
method destroy (line 63) | @Override
method onError (line 68) | @Override
FILE: app/src/main/java/com/kodelabs/mycosts/presentation/presenters/impl/EditCostPresenterImpl.java
class EditCostPresenterImpl (line 19) | public class EditCostPresenterImpl extends AbstractPresenter
method EditCostPresenterImpl (line 25) | public EditCostPresenterImpl(Executor executor, MainThread mainThread,
method getCostById (line 32) | @Override
method onCostRetrieved (line 45) | @Override
method noCostFound (line 50) | @Override
method editCost (line 55) | @Override
method onCostUpdated (line 68) | @Override
FILE: app/src/main/java/com/kodelabs/mycosts/presentation/presenters/impl/MainPresenterImpl.java
class MainPresenterImpl (line 21) | public class MainPresenterImpl extends AbstractPresenter implements Main...
method MainPresenterImpl (line 28) | public MainPresenterImpl(Executor executor, MainThread mainThread,
method resume (line 35) | @Override
method pause (line 40) | @Override
method stop (line 45) | @Override
method destroy (line 50) | @Override
method onError (line 55) | @Override
method getAllCosts (line 60) | @Override
method onCostsRetrieved (line 72) | @Override
method deleteCost (line 78) | @Override
method onCostDeleted (line 93) | @Override
FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/BaseView.java
type BaseView (line 8) | public interface BaseView {
method showProgress (line 14) | void showProgress();
method hideProgress (line 19) | void hideProgress();
method showError (line 26) | void showError(String message);
FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/activities/AboutActivity.java
class AboutActivity (line 13) | public class AboutActivity extends AppCompatActivity {
method onCreate (line 15) | @Override
FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/activities/AbstractCostActivity.java
class AbstractCostActivity (line 29) | public abstract class AbstractCostActivity extends AppCompatActivity
method onCreate (line 56) | @Override
method showDatePickerDialog (line 75) | @OnClick(R.id.input_date)
method onCreateOptionsMenu (line 83) | @Override
method extractFormData (line 90) | protected void extractFormData() {
method onDateSet (line 103) | @Override
FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/activities/AddCostActivity.java
class AddCostActivity (line 15) | public class AddCostActivity extends AbstractCostActivity
method onCreate (line 20) | @Override
method onResume (line 33) | @Override
method onOptionsItemSelected (line 42) | @Override
method onCostAdded (line 63) | @Override
method showProgress (line 69) | @Override
method hideProgress (line 74) | @Override
method showError (line 79) | @Override
FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/activities/EditCostActivity.java
class EditCostActivity (line 21) | public class EditCostActivity extends AbstractCostActivity implements Ed...
method onCreate (line 26) | @Override
method onCostRetrieved (line 52) | @Override
method findCategoryPosition (line 73) | private int findCategoryPosition(String category) {
method prepopulateFields (line 83) | private void prepopulateFields() {
method onOptionsItemSelected (line 95) | @Override
method onCostUpdated (line 116) | @Override
method showProgress (line 128) | @Override
method hideProgress (line 133) | @Override
method showError (line 138) | @Override
FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/activities/MainActivity.java
class MainActivity (line 35) | public class MainActivity extends AppCompatActivity implements MainPrese...
method onCreate (line 51) | @Override
method init (line 61) | private void init() {
method onResume (line 87) | @Override
method onCreateOptionsMenu (line 99) | @Override
method onOptionsItemSelected (line 108) | @Override
method onActivityResult (line 135) | @Override
method showCosts (line 147) | @Override
method onClickDeleteCost (line 153) | @Override
method onCostDeleted (line 177) | @Override
method onClickEditCost (line 183) | @Override
method showProgress (line 193) | @Override
method hideProgress (line 198) | @Override
method showError (line 203) | @Override
FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/adapters/CostItemAdapter.java
class CostItemAdapter (line 30) | public class CostItemAdapter extends RecyclerView.Adapter<RecyclerView.V...
type ViewType (line 33) | private enum ViewType {
class ViewHolder (line 44) | public static class ViewHolder extends RecyclerView.ViewHolder impleme...
method setup (line 54) | public void setup(DailyTotalCost dailyTotalCost) {
method onClick (line 63) | @Override
method ViewHolder (line 68) | public ViewHolder(View v, final RecyclerViewClickListener listener) {
class ExpandedViewHolder (line 76) | public static class ExpandedViewHolder extends RecyclerView.ViewHolder
method onClickDelete (line 84) | @Override
method onClickEdit (line 89) | @Override
method onClick (line 94) | @Override
method ExpandedViewHolder (line 99) | public ExpandedViewHolder(View v, final RecyclerViewClickListener li...
method CostItemAdapter (line 113) | public CostItemAdapter(MainPresenter.View view, Context context) {
method getItemViewType (line 120) | @Override
method onClickView (line 129) | @Override
method onClickDelete (line 143) | @Override
method onClickEdit (line 154) | @Override
method addNewCosts (line 159) | public void addNewCosts(@NonNull List<DailyTotalCost> costList) {
method onCreateViewHolder (line 169) | @Override
method onBindViewHolder (line 185) | @Override
method getItemCount (line 197) | @Override
FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/customviews/CostItemView.java
class CostItemView (line 24) | public class CostItemView extends RelativeLayout implements OnMenuItemCl...
method CostItemView (line 42) | public CostItemView(Context context,
method init (line 50) | private void init(Context context) {
method onMenuItemClick (line 63) | @Override
method onClickMenu (line 83) | @OnClick(R.id.button_menu)
method setCategory (line 91) | private void setCategory(String category) {
method setValue (line 95) | private void setValue(double value) {
method setDescription (line 100) | private void setDescription(String description) {
FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/customviews/ExpandedCostView.java
class ExpandedCostView (line 28) | public class ExpandedCostView extends CardView {
method ExpandedCostView (line 42) | public ExpandedCostView(Context context) {
method ExpandedCostView (line 47) | public ExpandedCostView(Context context, AttributeSet attrs) {
method ExpandedCostView (line 52) | public ExpandedCostView(Context context, AttributeSet attrs, int defSt...
method init (line 57) | private void init(Context context) {
method setIndividualCostViewClickListener (line 65) | public void setIndividualCostViewClickListener(
method addCostItem (line 70) | private void addCostItem(Cost cost, int position) {
method setTitle (line 81) | private void setTitle(Date date) {
method setTotalValue (line 87) | private void setTotalValue(double value) {
method setDailyCost (line 92) | public void setDailyCost(DailyTotalCost dailyCost) {
FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/fragments/DatePickerFragment.java
class DatePickerFragment (line 14) | public class DatePickerFragment extends DialogFragment {
method DatePickerFragment (line 16) | public DatePickerFragment() {
method setListener (line 22) | public void setListener(OnDateSetListener listener) {
method onCreateDialog (line 26) | @Override
FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/listeners/IndividualCostViewClickListener.java
type IndividualCostViewClickListener (line 6) | public interface IndividualCostViewClickListener {
method onClickDelete (line 8) | void onClickDelete(long costId);
method onClickEdit (line 10) | void onClickEdit(long costId);
FILE: app/src/main/java/com/kodelabs/mycosts/presentation/ui/listeners/RecyclerViewClickListener.java
type RecyclerViewClickListener (line 6) | public interface RecyclerViewClickListener {
method onClickView (line 8) | void onClickView(int position);
method onClickEdit (line 10) | void onClickEdit(int position, long costId);
method onClickDelete (line 12) | void onClickDelete(int position, long costId);
FILE: app/src/main/java/com/kodelabs/mycosts/storage/CostRepositoryImpl.java
class CostRepositoryImpl (line 20) | public class CostRepositoryImpl implements CostRepository {
method CostRepositoryImpl (line 69) | public CostRepositoryImpl(Context context) {
method insert (line 74) | @Override
method update (line 85) | @Override
method getCostById (line 96) | @Override
method getAllCosts (line 107) | @Override
method getAllUnsyncedCosts (line 119) | @Override
method markSynced (line 131) | @Override
method delete (line 143) | @Override
FILE: app/src/main/java/com/kodelabs/mycosts/storage/contentprovider/StubProvider.java
class StubProvider (line 13) | public class StubProvider extends ContentProvider {
method onCreate (line 18) | @Override
method getType (line 26) | @Override
method query (line 35) | @Override
method insert (line 48) | @Override
method delete (line 56) | @Override
method update (line 64) | public int update(
FILE: app/src/main/java/com/kodelabs/mycosts/storage/converters/StorageModelConverter.java
class StorageModelConverter (line 12) | public class StorageModelConverter {
method convertToStorageModel (line 14) | public static Cost convertToStorageModel(com.kodelabs.mycosts.domain.m...
method convertToDomainModel (line 25) | public static com.kodelabs.mycosts.domain.model.Cost convertToDomainMo...
method convertListToDomainModel (line 45) | public static List<com.kodelabs.mycosts.domain.model.Cost> convertList...
method convertListToStorageModel (line 60) | public static List<Cost> convertListToStorageModel(List<com.kodelabs.m...
FILE: app/src/main/java/com/kodelabs/mycosts/storage/database/CostDatabase.java
class CostDatabase (line 8) | @Database(name = CostDatabase.NAME, version = CostDatabase.VERSION)
FILE: app/src/main/java/com/kodelabs/mycosts/storage/model/Cost.java
class Cost (line 14) | @Table(database = CostDatabase.class)
method Cost (line 36) | public Cost() {
method Cost (line 42) | public Cost(String category, String description, Date date, double amo...
method getId (line 52) | public long getId() {
method setId (line 56) | public void setId(long id) {
method getCategory (line 60) | public String getCategory() {
method setCategory (line 64) | public void setCategory(String category) {
method getDescription (line 68) | public String getDescription() {
method setDescription (line 72) | public void setDescription(String description) {
method getDate (line 76) | public Date getDate() {
method setDate (line 80) | public void setDate(Date date) {
method getAmount (line 84) | public double getAmount() {
method setAmount (line 88) | public void setAmount(double amount) {
method toString (line 93) | @Override
FILE: app/src/main/java/com/kodelabs/mycosts/sync/SyncAdapter.java
class SyncAdapter (line 35) | public class SyncAdapter extends AbstractThreadedSyncAdapter {
method SyncAdapter (line 43) | public SyncAdapter(Context context, boolean autoInitialize) {
method SyncAdapter (line 49) | public SyncAdapter(Context context, boolean autoInitialize, boolean al...
method triggerSync (line 58) | public static void triggerSync(Context context) {
method onResponse (line 74) | @Override
method onFailure (line 83) | @Override
method onPerformSync (line 93) | @Override
FILE: app/src/main/java/com/kodelabs/mycosts/sync/SyncService.java
class SyncService (line 12) | public class SyncService extends Service {
method onCreate (line 22) | @Override
method onBind (line 40) | @Nullable
FILE: app/src/main/java/com/kodelabs/mycosts/sync/auth/Authenticator.java
class Authenticator (line 16) | public class Authenticator extends AbstractAccountAuthenticator {
method Authenticator (line 18) | public Authenticator(Context context) {
method editProperties (line 23) | @Override
method addAccount (line 30) | @Override
method confirmCredentials (line 41) | @Override
method getAuthToken (line 50) | @Override
method getAuthTokenLabel (line 60) | @Override
method updateCredentials (line 66) | @Override
method hasFeatures (line 75) | @Override
FILE: app/src/main/java/com/kodelabs/mycosts/sync/auth/AuthenticatorService.java
class AuthenticatorService (line 11) | public class AuthenticatorService extends Service {
method onCreate (line 17) | @Override
method onBind (line 27) | @Override
FILE: app/src/main/java/com/kodelabs/mycosts/sync/auth/DummyAccountProvider.java
class DummyAccountProvider (line 12) | public class DummyAccountProvider {
method getDummyAccount (line 14) | public static Account getDummyAccount(Context context) {
method CreateSyncAccount (line 28) | public static boolean CreateSyncAccount(Context context) {
FILE: app/src/main/java/com/kodelabs/mycosts/threading/MainThreadImpl.java
class MainThreadImpl (line 13) | public class MainThreadImpl implements MainThread {
method MainThreadImpl (line 19) | private MainThreadImpl() {
method post (line 23) | @Override
method getInstance (line 28) | public static MainThread getInstance() {
FILE: app/src/main/java/com/kodelabs/mycosts/utils/AuthUtils.java
class AuthUtils (line 16) | public class AuthUtils {
method createDummyAccount (line 21) | public static Account createDummyAccount(Context context) {
method getAccount (line 57) | public static Account getAccount(Context context) {
FILE: app/src/main/java/com/kodelabs/mycosts/utils/DateUtils.java
class DateUtils (line 15) | public class DateUtils {
method dateToText (line 24) | public static String dateToText(Context context, Date date) {
method createDate (line 57) | public static Date createDate(int year, int monthOfYear, int dayOfMont...
method formatDate (line 76) | public static String formatDate(Date date) {
method formatDate (line 81) | public static String formatDate(Date date, SimpleDateFormat sdf) {
method getToday (line 85) | public static Date getToday() {
method truncateHours (line 101) | public static Date truncateHours(Date date) {
FILE: app/src/test/java/com/kodelabs/mycosts/domain/interactors/GetCostByIdTest.java
class GetCostByIdTest (line 24) | public class GetCostByIdTest {
method setUp (line 33) | @Before
method testCostNotFound (line 40) | @Test
method testCostFound (line 50) | @Test
FILE: app/src/test/java/com/kodelabs/mycosts/presentation/converter/DailyTotalCostConverterTest.java
class DailyTotalCostConverterTest (line 18) | public class DailyTotalCostConverterTest {
method testDailyCostConversion (line 22) | @Test
method testDailyCostConversion2 (line 57) | @Test
FILE: app/src/test/java/com/kodelabs/mycosts/threading/TestMainThread.java
class TestMainThread (line 8) | public class TestMainThread implements MainThread {
method post (line 10) | @Override
FILE: app/src/test/java/com/kodelabs/mycosts/util/TestDateUtil.java
class TestDateUtil (line 9) | public class TestDateUtil {
method getDate (line 11) | public static Date getDate(int year, int month, int day) {
Condensed preview — 111 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (189K chars).
[
{
"path": ".gitignore",
"chars": 405,
"preview": "*.iml\n.gradle\n/local.properties\n/.idea/workspace.xml\n/.idea/libraries\n.DS_Store\n/build\n/captures\n=======\n# Built applica"
},
{
"path": ".idea/.name",
"chars": 8,
"preview": "My Costs"
},
{
"path": ".idea/codeStyleSettings.xml",
"chars": 8900,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"ProjectCodeStyleSettingsManager\">\n <o"
},
{
"path": ".idea/compiler.xml",
"chars": 709,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"CompilerConfiguration\">\n <option name"
},
{
"path": ".idea/copyright/profiles_settings.xml",
"chars": 74,
"preview": "<component name=\"CopyrightManager\">\n <settings default=\"\" />\n</component>"
},
{
"path": ".idea/gradle.xml",
"chars": 684,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"GradleSettings\">\n <option name=\"linke"
},
{
"path": ".idea/misc.xml",
"chars": 3952,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"EntryPointsManager\">\n <entry_points v"
},
{
"path": ".idea/modules.xml",
"chars": 351,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"ProjectModuleManager\">\n <modules>\n "
},
{
"path": ".idea/runConfigurations.xml",
"chars": 564,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"RunConfigurationProducerService\">\n <o"
},
{
"path": ".idea/vcs.xml",
"chars": 2312,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"VcsDirectoryMappings\">\n <mapping dire"
},
{
"path": "LICENSE",
"chars": 1081,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2015 Dario Miličić\n\nPermission is hereby granted, free of charge, to any person obt"
},
{
"path": "QA/findbugs/findbugs-filter.xml",
"chars": 615,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<FindBugsFilter>\n <Match>\n <!-- ignore all issues in resource generatio"
},
{
"path": "QA/quality.gradle",
"chars": 541,
"preview": "apply plugin: 'findbugs'\n\ntask findbugs(type: FindBugs) {\n ignoreFailures = true\n effort = \"default\"\n reportLev"
},
{
"path": "README.md",
"chars": 5423,
"preview": "# Android Clean - Cost Tracker\nA sample cost-tracker app that showcases my Clean architecture approach to build Android "
},
{
"path": "app/.gitignore",
"chars": 7,
"preview": "/build\n"
},
{
"path": "app/build.gradle",
"chars": 1741,
"preview": "apply plugin: 'com.android.application'\napply plugin: 'com.neenbedankt.android-apt'\napply from: \"${project.rootDir}/QA/q"
},
{
"path": "app/proguard-rules.pro",
"chars": 675,
"preview": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /U"
},
{
"path": "app/src/androidTest/java/com/kodelabs/mycosts/ApplicationTest.java",
"chars": 351,
"preview": "package com.kodelabs.mycosts;\n\nimport android.app.Application;\nimport android.test.ApplicationTestCase;\n\n/**\n * <a href="
},
{
"path": "app/src/main/AndroidManifest.xml",
"chars": 3055,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest\n package=\"com.kodelabs.mycosts\"\n xmlns:android=\"http://schemas.an"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/AndroidApplication.java",
"chars": 589,
"preview": "package com.kodelabs.mycosts;\n\nimport android.app.Application;\n\nimport com.facebook.stetho.Stetho;\nimport com.raizlabs.a"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/domain/executor/Executor.java",
"chars": 593,
"preview": "package com.kodelabs.mycosts.domain.executor;\n\nimport com.kodelabs.mycosts.domain.interactors.base.AbstractInteractor;\n\n"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/domain/executor/MainThread.java",
"chars": 555,
"preview": "package com.kodelabs.mycosts.domain.executor;\n\n/**\n * This interface will define a class that will enable interactors to"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/domain/executor/impl/ThreadExecutor.java",
"chars": 2094,
"preview": "package com.kodelabs.mycosts.domain.executor.impl;\n\nimport com.kodelabs.mycosts.domain.executor.Executor;\nimport com.kod"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/domain/interactors/AddCostInteractor.java",
"chars": 277,
"preview": "package com.kodelabs.mycosts.domain.interactors;\n\nimport com.kodelabs.mycosts.domain.interactors.base.Interactor;\n\n/**\n "
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/domain/interactors/DeleteCostInteractor.java",
"chars": 338,
"preview": "package com.kodelabs.mycosts.domain.interactors;\n\nimport com.kodelabs.mycosts.domain.interactors.base.Interactor;\nimport"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/domain/interactors/EditCostInteractor.java",
"chars": 337,
"preview": "package com.kodelabs.mycosts.domain.interactors;\n\nimport com.kodelabs.mycosts.domain.interactors.base.Interactor;\nimport"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/domain/interactors/GetAllCostsInteractor.java",
"chars": 468,
"preview": "package com.kodelabs.mycosts.domain.interactors;\n\nimport com.kodelabs.mycosts.domain.interactors.base.Interactor;\nimport"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/domain/interactors/GetCostByIdInteractor.java",
"chars": 370,
"preview": "package com.kodelabs.mycosts.domain.interactors;\n\nimport com.kodelabs.mycosts.domain.interactors.base.Interactor;\nimport"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/domain/interactors/base/AbstractInteractor.java",
"chars": 2123,
"preview": "package com.kodelabs.mycosts.domain.interactors.base;\n\nimport com.kodelabs.mycosts.domain.executor.Executor;\nimport com."
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/domain/interactors/base/Interactor.java",
"chars": 313,
"preview": "package com.kodelabs.mycosts.domain.interactors.base;\n\n/**\n * Created by dmilicic on 12/13/15.\n */\npublic interface Inte"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/domain/interactors/impl/AddCostInteractorImpl.java",
"chars": 1946,
"preview": "package com.kodelabs.mycosts.domain.interactors.impl;\n\nimport com.kodelabs.mycosts.domain.executor.Executor;\nimport com."
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/domain/interactors/impl/DeleteCostInteractorImpl.java",
"chars": 1618,
"preview": "package com.kodelabs.mycosts.domain.interactors.impl;\n\nimport com.kodelabs.mycosts.domain.executor.Executor;\nimport com."
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/domain/interactors/impl/EditCostInteractorImpl.java",
"chars": 2581,
"preview": "package com.kodelabs.mycosts.domain.interactors.impl;\n\nimport com.kodelabs.mycosts.domain.executor.Executor;\nimport com."
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/domain/interactors/impl/GetAllCostsInteractorImpl.java",
"chars": 2218,
"preview": "package com.kodelabs.mycosts.domain.interactors.impl;\n\nimport com.kodelabs.mycosts.domain.executor.Executor;\nimport com."
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/domain/interactors/impl/GetCostByIdInteractorImpl.java",
"chars": 1965,
"preview": "package com.kodelabs.mycosts.domain.interactors.impl;\n\nimport com.kodelabs.mycosts.domain.executor.Executor;\nimport com."
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/domain/model/Cost.java",
"chars": 2253,
"preview": "package com.kodelabs.mycosts.domain.model;\n\nimport java.util.Date;\n\n/**\n * Created by dmilicic on 12/10/15.\n */\npublic c"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/domain/repository/CostRepository.java",
"chars": 430,
"preview": "package com.kodelabs.mycosts.domain.repository;\n\nimport com.kodelabs.mycosts.domain.model.Cost;\n\nimport java.util.List;\n"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/network/RestClient.java",
"chars": 1405,
"preview": "package com.kodelabs.mycosts.network;\n\nimport com.facebook.stetho.okhttp3.StethoInterceptor;\n\nimport okhttp3.OkHttpClien"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/network/converters/RESTModelConverter.java",
"chars": 585,
"preview": "package com.kodelabs.mycosts.network.converters;\n\nimport com.kodelabs.mycosts.domain.model.Cost;\nimport com.kodelabs.myc"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/network/model/Payload.java",
"chars": 1272,
"preview": "package com.kodelabs.mycosts.network.model;\n\nimport com.google.gson.Gson;\nimport com.google.gson.annotations.SerializedN"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/network/model/RESTCost.java",
"chars": 1030,
"preview": "package com.kodelabs.mycosts.network.model;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport java.util.Date;\n"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/network/services/SyncService.java",
"chars": 526,
"preview": "package com.kodelabs.mycosts.network.services;\n\nimport com.kodelabs.mycosts.network.model.Payload;\n\nimport retrofit2.Cal"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/presentation/animation/AnimatorFactory.java",
"chars": 3426,
"preview": "package com.kodelabs.mycosts.presentation.animation;\n\nimport android.animation.Animator;\nimport android.animation.Animat"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/presentation/converter/DailyTotalCostConverter.java",
"chars": 2220,
"preview": "package com.kodelabs.mycosts.presentation.converter;\n\nimport com.kodelabs.mycosts.domain.model.Cost;\nimport com.kodelabs"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/presentation/model/DailyTotalCost.java",
"chars": 1059,
"preview": "package com.kodelabs.mycosts.presentation.model;\n\nimport com.kodelabs.mycosts.domain.model.Cost;\n\nimport java.util.Date;"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/presentation/presenters/AbstractPresenter.java",
"chars": 472,
"preview": "package com.kodelabs.mycosts.presentation.presenters;\n\nimport com.kodelabs.mycosts.domain.executor.Executor;\nimport com."
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/presentation/presenters/AddCostPresenter.java",
"chars": 399,
"preview": "package com.kodelabs.mycosts.presentation.presenters;\n\nimport com.kodelabs.mycosts.presentation.ui.BaseView;\n\nimport jav"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/presentation/presenters/BasePresenter.java",
"chars": 985,
"preview": "package com.kodelabs.mycosts.presentation.presenters;\n\n/**\n * Created by dmilicic on 7/28/15.\n */\npublic interface BaseP"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/presentation/presenters/EditCostPresenter.java",
"chars": 517,
"preview": "package com.kodelabs.mycosts.presentation.presenters;\n\nimport com.kodelabs.mycosts.domain.model.Cost;\nimport com.kodelab"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/presentation/presenters/MainPresenter.java",
"chars": 647,
"preview": "package com.kodelabs.mycosts.presentation.presenters;\n\nimport com.kodelabs.mycosts.domain.model.Cost;\nimport com.kodelab"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/presentation/presenters/impl/AddCostPresenterImpl.java",
"chars": 1822,
"preview": "package com.kodelabs.mycosts.presentation.presenters.impl;\n\nimport com.kodelabs.mycosts.domain.executor.Executor;\nimport"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/presentation/presenters/impl/EditCostPresenterImpl.java",
"chars": 2361,
"preview": "package com.kodelabs.mycosts.presentation.presenters.impl;\n\nimport com.kodelabs.mycosts.domain.executor.Executor;\nimport"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/presentation/presenters/impl/MainPresenterImpl.java",
"chars": 2737,
"preview": "package com.kodelabs.mycosts.presentation.presenters.impl;\n\nimport com.kodelabs.mycosts.domain.executor.Executor;\nimport"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/presentation/ui/BaseView.java",
"chars": 803,
"preview": "package com.kodelabs.mycosts.presentation.ui;\n\n/**\n * Created by dmilicic on 7/28/15.\n * <p/>\n * This interface represen"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/presentation/ui/activities/AboutActivity.java",
"chars": 1271,
"preview": "package com.kodelabs.mycosts.presentation.ui.activities;\n\nimport android.content.Intent;\nimport android.net.Uri;\nimport "
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/presentation/ui/activities/AbstractCostActivity.java",
"chars": 3154,
"preview": "package com.kodelabs.mycosts.presentation.ui.activities;\n\nimport android.app.DatePickerDialog;\nimport android.os.Bundle;"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/presentation/ui/activities/AddCostActivity.java",
"chars": 2272,
"preview": "package com.kodelabs.mycosts.presentation.ui.activities;\n\nimport android.os.Bundle;\nimport android.view.MenuItem;\nimport"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/presentation/ui/activities/EditCostActivity.java",
"chars": 4291,
"preview": "package com.kodelabs.mycosts.presentation.ui.activities;\n\nimport android.content.Intent;\nimport android.os.Bundle;\nimpor"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/presentation/ui/activities/MainActivity.java",
"chars": 6351,
"preview": "package com.kodelabs.mycosts.presentation.ui.activities;\n\nimport android.app.AlertDialog;\nimport android.content.DialogI"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/presentation/ui/adapters/CostItemAdapter.java",
"chars": 6585,
"preview": "package com.kodelabs.mycosts.presentation.ui.adapters;\n\nimport android.content.Context;\nimport android.support.annotatio"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/presentation/ui/customviews/CostItemView.java",
"chars": 3013,
"preview": "package com.kodelabs.mycosts.presentation.ui.customviews;\n\nimport android.content.Context;\nimport android.view.LayoutInf"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/presentation/ui/customviews/ExpandedCostView.java",
"chars": 3415,
"preview": "package com.kodelabs.mycosts.presentation.ui.customviews;\n\nimport android.content.Context;\nimport android.support.annota"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/presentation/ui/fragments/DatePickerFragment.java",
"chars": 1062,
"preview": "package com.kodelabs.mycosts.presentation.ui.fragments;\n\nimport android.app.DatePickerDialog;\nimport android.app.DatePic"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/presentation/ui/listeners/IndividualCostViewClickListener.java",
"chars": 226,
"preview": "package com.kodelabs.mycosts.presentation.ui.listeners;\n\n/**\n * Created by dmilicic on 1/6/16.\n */\npublic interface Indi"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/presentation/ui/listeners/RecyclerViewClickListener.java",
"chars": 287,
"preview": "package com.kodelabs.mycosts.presentation.ui.listeners;\n\n/**\n * Created by dmilicic on 12/26/15.\n */\npublic interface Re"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/storage/CostRepositoryImpl.java",
"chars": 5060,
"preview": "package com.kodelabs.mycosts.storage;\n\nimport android.content.Context;\n\nimport com.kodelabs.mycosts.domain.model.Cost;\ni"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/storage/contentprovider/StubProvider.java",
"chars": 1525,
"preview": "package com.kodelabs.mycosts.storage.contentprovider;\n\nimport android.content.ContentProvider;\nimport android.content.Co"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/storage/converters/StorageModelConverter.java",
"chars": 2000,
"preview": "package com.kodelabs.mycosts.storage.converters;\n\nimport com.kodelabs.mycosts.storage.model.Cost;\n\nimport java.util.Arra"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/storage/database/CostDatabase.java",
"chars": 338,
"preview": "package com.kodelabs.mycosts.storage.database;\n\nimport com.raizlabs.android.dbflow.annotation.Database;\n\n/**\n * Created "
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/storage/model/Cost.java",
"chars": 2286,
"preview": "package com.kodelabs.mycosts.storage.model;\n\nimport com.kodelabs.mycosts.storage.database.CostDatabase;\nimport com.raizl"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/sync/SyncAdapter.java",
"chars": 4572,
"preview": "package com.kodelabs.mycosts.sync;\n\nimport android.accounts.Account;\nimport android.content.AbstractThreadedSyncAdapter;"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/sync/SyncService.java",
"chars": 1498,
"preview": "package com.kodelabs.mycosts.sync;\n\nimport android.app.Service;\nimport android.content.Intent;\nimport android.os.IBinder"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/sync/auth/Authenticator.java",
"chars": 2472,
"preview": "package com.kodelabs.mycosts.sync.auth;\n\nimport android.accounts.AbstractAccountAuthenticator;\nimport android.accounts.A"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/sync/auth/AuthenticatorService.java",
"chars": 757,
"preview": "package com.kodelabs.mycosts.sync.auth;\n\nimport android.app.Service;\nimport android.content.Intent;\nimport android.os.IB"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/sync/auth/DummyAccountProvider.java",
"chars": 1352,
"preview": "package com.kodelabs.mycosts.sync.auth;\n\nimport android.accounts.Account;\nimport android.accounts.AccountManager;\nimport"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/threading/MainThreadImpl.java",
"chars": 785,
"preview": "package com.kodelabs.mycosts.threading;\n\nimport android.os.Handler;\nimport android.os.Looper;\n\nimport com.kodelabs.mycos"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/utils/AuthUtils.java",
"chars": 2416,
"preview": "package com.kodelabs.mycosts.utils;\n\nimport android.accounts.Account;\nimport android.accounts.AccountManager;\nimport and"
},
{
"path": "app/src/main/java/com/kodelabs/mycosts/utils/DateUtils.java",
"chars": 3198,
"preview": "package com.kodelabs.mycosts.utils;\n\nimport android.content.Context;\n\nimport com.kodelabs.mycosts.R;\n\nimport java.text.S"
},
{
"path": "app/src/main/res/anim/hold.xml",
"chars": 254,
"preview": "<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:shareInterpolator=\"false\">\n\n <translate\n "
},
{
"path": "app/src/main/res/drawable/rounded_corner.xml",
"chars": 464,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <solid and"
},
{
"path": "app/src/main/res/layout/activity_about.xml",
"chars": 1427,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<android.support.design.widget.CoordinatorLayout xmlns:android=\"http://schemas.an"
},
{
"path": "app/src/main/res/layout/activity_add_cost.xml",
"chars": 1184,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns"
},
{
"path": "app/src/main/res/layout/activity_main.xml",
"chars": 1188,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<io.codetail.widget.RevealFrameLayout xmlns:android=\"http://schemas.android.com/a"
},
{
"path": "app/src/main/res/layout/card_daily_cost_item.xml",
"chars": 1335,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<android.support.v7.widget.CardView\n android:id=\"@+id/cost_item_card\"\n xml"
},
{
"path": "app/src/main/res/layout/card_expanded_daily_cost_item.xml",
"chars": 544,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.kodelabs.mycosts.presentation.ui.customviews.ExpandedCostView\n android:id"
},
{
"path": "app/src/main/res/layout/content_about.xml",
"chars": 1409,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xm"
},
{
"path": "app/src/main/res/layout/content_add_cost.xml",
"chars": 3304,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<io.codetail.widget.RevealFrameLayout\n android:id=\"@+id/reveal_layout\"\n xml"
},
{
"path": "app/src/main/res/layout/expanded_cost_item.xml",
"chars": 2038,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<merge xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:lay"
},
{
"path": "app/src/main/res/layout/individual_cost_item.xml",
"chars": 1817,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n an"
},
{
"path": "app/src/main/res/menu/menu_add_cost.xml",
"chars": 318,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app=\"h"
},
{
"path": "app/src/main/res/menu/menu_cost_item.xml",
"chars": 481,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app=\"h"
},
{
"path": "app/src/main/res/menu/menu_main.xml",
"chars": 697,
"preview": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app=\"http://schemas.android.com/apk/res-auto\""
},
{
"path": "app/src/main/res/values/colors.xml",
"chars": 258,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"colorPrimary\">#3F51B5</color>\n <color name=\"color"
},
{
"path": "app/src/main/res/values/dimens.xml",
"chars": 253,
"preview": "<resources>\n <!-- Default screen margins, per the Android Design guidelines. -->\n <dimen name=\"activity_horizontal"
},
{
"path": "app/src/main/res/values/strings.xml",
"chars": 1388,
"preview": "<resources>\n <string name=\"app_name\">My Costs</string>\n\n <!-- Actions -->\n <string name=\"action_about\">About</s"
},
{
"path": "app/src/main/res/values/styles.xml",
"chars": 641,
"preview": "<resources>\n\n <!-- Base application theme. -->\n <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light\">\n <!-"
},
{
"path": "app/src/main/res/values-v21/styles.xml",
"chars": 328,
"preview": "<resources>>\n\n <style name=\"AppTheme.NoActionBar\">\n <item name=\"windowActionBar\">false</item>\n <item na"
},
{
"path": "app/src/main/res/values-w820dp/dimens.xml",
"chars": 358,
"preview": "<resources>\n <!-- Example customization of dimensions originally defined in res/values/dimens.xml\n (such as s"
},
{
"path": "app/src/main/res/xml/authenticator.xml",
"chars": 316,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<account-authenticator xmlns:android=\"http://schemas.android.com/apk/res/android\""
},
{
"path": "app/src/main/res/xml/syncadapter.xml",
"chars": 367,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<sync-adapter xmlns:android=\"http://schemas.android.com/apk/res/android\"\n andr"
},
{
"path": "app/src/test/java/com/kodelabs/mycosts/domain/interactors/GetCostByIdTest.java",
"chars": 2195,
"preview": "package com.kodelabs.mycosts.domain.interactors;\n\nimport com.kodelabs.mycosts.domain.executor.Executor;\nimport com.kodel"
},
{
"path": "app/src/test/java/com/kodelabs/mycosts/presentation/converter/DailyTotalCostConverterTest.java",
"chars": 4134,
"preview": "package com.kodelabs.mycosts.presentation.converter;\n\nimport com.kodelabs.mycosts.domain.model.Cost;\nimport com.kodelabs"
},
{
"path": "app/src/test/java/com/kodelabs/mycosts/threading/TestMainThread.java",
"chars": 354,
"preview": "package com.kodelabs.mycosts.threading;\n\nimport com.kodelabs.mycosts.domain.executor.MainThread;\n\n/**\n * Created by dmil"
},
{
"path": "app/src/test/java/com/kodelabs/mycosts/util/TestDateUtil.java",
"chars": 581,
"preview": "package com.kodelabs.mycosts.util;\n\nimport java.util.Calendar;\nimport java.util.Date;\n\n/**\n * Created by dmilicic on 1/9"
},
{
"path": "build.gradle",
"chars": 629,
"preview": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n r"
},
{
"path": "gradle/wrapper/gradle-wrapper.properties",
"chars": 230,
"preview": "#Wed Oct 21 11:34:03 PDT 2015\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_"
},
{
"path": "gradle.properties",
"chars": 855,
"preview": "# Project-wide Gradle settings.\n\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will o"
},
{
"path": "gradlew",
"chars": 4971,
"preview": "#!/usr/bin/env bash\n\n##############################################################################\n##\n## Gradle start "
},
{
"path": "gradlew.bat",
"chars": 2404,
"preview": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@r"
},
{
"path": "settings.gradle",
"chars": 15,
"preview": "include ':app'\n"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the dmilicic/android-clean-sample-app GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 111 files (167.7 KB), approximately 43.0k tokens, and a symbol index with 337 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.