├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── debug │ └── res │ │ └── values │ │ └── strings.xml │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── morcinek │ │ │ └── workout │ │ │ ├── Application.kt │ │ │ ├── common │ │ │ ├── FunctionalCountDownTimer.kt │ │ │ ├── NotificationCenter.kt │ │ │ ├── di │ │ │ │ ├── ActivityScope.kt │ │ │ │ ├── ApplicationComponent.kt │ │ │ │ ├── DaggerUtils.kt │ │ │ │ └── modules │ │ │ │ │ ├── AndroidModule.kt │ │ │ │ │ └── ApplicationModule.kt │ │ │ ├── firebase │ │ │ │ ├── auth │ │ │ │ │ └── UserManager.kt │ │ │ │ ├── data │ │ │ │ │ ├── DataManager.kt │ │ │ │ │ ├── DataManagerImpl.kt │ │ │ │ │ ├── DataModel.kt │ │ │ │ │ ├── DataProvider.kt │ │ │ │ │ └── InteractorDelegate.kt │ │ │ │ ├── database │ │ │ │ │ └── ReferenceManager.kt │ │ │ │ └── di │ │ │ │ │ └── modules │ │ │ │ │ └── FirebaseModule.kt │ │ │ ├── fragment │ │ │ │ ├── BaseFragment.kt │ │ │ │ ├── ContentFragmentManager.kt │ │ │ │ └── SingleFragmentActivity.kt │ │ │ ├── preference │ │ │ │ ├── TimePreference.kt │ │ │ │ └── TimePreferenceDialogFragmentCompat.kt │ │ │ └── utils │ │ │ │ ├── AndroidUtils.kt │ │ │ │ ├── CalendarUtils.kt │ │ │ │ ├── FragmentUtils.kt │ │ │ │ ├── IntentUtils.kt │ │ │ │ ├── SharedPreferencesUtils.kt │ │ │ │ └── StartActivityUtils.kt │ │ │ ├── core │ │ │ └── data │ │ │ │ ├── DataModule.kt │ │ │ │ ├── DataUtils.kt │ │ │ │ └── exercises │ │ │ │ ├── ExerciseDataModel.kt │ │ │ │ ├── ExerciseManager.kt │ │ │ │ └── ExercisesProvider.kt │ │ │ ├── exercise │ │ │ ├── ExerciseActivity.kt │ │ │ ├── ExerciseDataManager.kt │ │ │ ├── ExerciseState.kt │ │ │ ├── TimerService.kt │ │ │ ├── di │ │ │ │ ├── ExerciseComponent.kt │ │ │ │ └── ExerciseModule.kt │ │ │ └── fragments │ │ │ │ ├── BreakFragment.kt │ │ │ │ ├── BreakSplashFragment.kt │ │ │ │ ├── LoadingFragment.kt │ │ │ │ ├── NewFragment.kt │ │ │ │ └── SeriesFragment.kt │ │ │ ├── home │ │ │ ├── HomeActivity.kt │ │ │ └── exercises │ │ │ │ ├── ExercisesFragment.kt │ │ │ │ ├── ExercisesInteractor.kt │ │ │ │ └── adapter │ │ │ │ └── ExerciseViewAdapter.kt │ │ │ ├── settings │ │ │ ├── SettingsFragment.kt │ │ │ └── SettingsPreferencesUtils.kt │ │ │ └── splash │ │ │ └── SplashActivity.kt │ └── res │ │ ├── drawable-v21 │ │ ├── oval_accent.xml │ │ └── oval_primary.xml │ │ ├── drawable-xhdpi │ │ ├── ic_delete.png │ │ ├── ic_edit.png │ │ ├── ic_settings.png │ │ └── logo.png │ │ ├── drawable-xxhdpi │ │ ├── ic_delete.png │ │ ├── ic_edit.png │ │ ├── ic_settings.png │ │ └── logo.png │ │ ├── drawable-xxxhdpi │ │ ├── ic_delete.png │ │ ├── ic_edit.png │ │ ├── ic_settings.png │ │ └── logo.png │ │ ├── drawable │ │ ├── oval_accent.xml │ │ ├── oval_primary.xml │ │ ├── progress_bar_accent.xml │ │ ├── progress_bar_background.xml │ │ └── splash_background.xml │ │ ├── layout │ │ ├── content_fragment.xml │ │ ├── content_home.xml │ │ ├── exercise_break.xml │ │ ├── exercise_break_splash.xml │ │ ├── exercise_loading.xml │ │ ├── exercise_new.xml │ │ ├── exercise_series.xml │ │ ├── exercises_item.xml │ │ ├── home.xml │ │ ├── pref_dialog_time.xml │ │ ├── recycler_view.xml │ │ └── spinner_layout.xml │ │ ├── menu │ │ ├── exercise.xml │ │ └── home.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-pl │ │ ├── arrays.xml │ │ └── strings.xml │ │ ├── values │ │ ├── arrays.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── themes.xml │ │ └── xml │ │ └── settings.xml │ └── test │ └── java │ └── com │ └── morcinek │ └── workout │ └── ExampleUnitTest.java ├── build.gradle ├── config └── debug.keystore ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/ 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | google-services.json 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Kotlin Android Tutorial 2 | ================ 3 | 4 | 5 | ## Content 6 | 7 | This is a tutorial project to show how to setup and use Kotlin in Android Project. 8 | I have recently started going to the gym, and I found out that it would be easier if I had 9 | application to assist me. I am calling this application 'Workout'. 10 | 11 | Work on this application goes in steps, each step either integrates some new functionality 12 | or demonstrates usage of specific feature of Kotlin Language. 13 | 14 | Each step has its own tag (name in the round brackets), and a corresponding video on Youtube. 15 | 16 | ### 1 Project Setup (project-setup) 17 | 18 | https://youtu.be/zfmT8fVlMhY 19 | 20 | - Creating new Project in Android Studio 21 | - Initialising GIT 22 | 23 | 24 | ### 2 Kotlin Integration (kotlin-integration) 25 | 26 | https://youtu.be/8HuBoPYb0gw 27 | 28 | - Adding Kotlin plugins to build.gradle 29 | - Converting Java Code into Kotlin Code 30 | 31 | 32 | ### 3 Kotlin View Injections (view-injections) 33 | 34 | https://youtu.be/cs4YZg3P4Ik 35 | 36 | - Adding Kotlin Android Extensions Plugin 37 | `apply plugin: 'kotlin-android-extensions'` 38 | - Using auto-generated properties for views in View Class (Activity) 39 | `import kotlinx.android.synthetic.main.home.*` 40 | 41 | 42 | ### 4 Anko Integration (anko-integration) 43 | 44 | https://youtu.be/SG77qTm38nw 45 | 46 | - Code Cleanup 47 | - Adding Anko library to the project 48 | `compile "org.jetbrains.anko:anko:$versions.anko"` 49 | - Examples of one line functions (Kotlin introduces specific syntax) 50 | `fun isThisAJoke() = "NO Its not a Joke"` 51 | 52 | 53 | ### 5 Splash Screen (splash-screen) 54 | 55 | https://youtu.be/kUI1LRmPCBo 56 | 57 | - Adding Anko Design library to the project 58 | `compile "org.jetbrains.anko:anko-design:$versions.anko"` 59 | - Examples of Anko usage: 60 | `snackbar` 61 | `doAsync` 62 | `uiThread` 63 | `startActivity()` 64 | 65 | 66 | ### 6 Splash Screen according to Google Guidelines (google-splash) 67 | 68 | https://youtu.be/r9KPvkAk0BM 69 | 70 | - Implementation of Splash Screen to enhance User Experience 71 | - Based on: https://www.bignerdranch.com/blog/splash-screens-the-right-way/ 72 | 73 | 74 | ### 7 Dagger Integration (dagger-integration) 75 | 76 | https://youtu.be/F0OwtSrhh0o 77 | 78 | - Integration of dagger into Android Kotlin Project. 79 | - Creation and usage of 'Extension Function' 80 | `fun AppCompatActivity.component() : ApplicationComponent = (application as Application).component` 81 | - Creation and usage of 'Extension Property' 82 | `val AppCompatActivity.component: ApplicationComponent 83 | get() = (application as Application).component` 84 | 85 | 86 | ### 8 Counter Initial (counter-initial) 87 | 88 | https://youtu.be/v7pe5JN_eLs 89 | 90 | - Using CountDownTimer for executing task after interval. 91 | - Introduction to 'val' property 92 | `val name: String = "tomasz" ` 93 | - Introduction to 'var' property 94 | `@Inject lateinit var context: Context` 95 | - Introduction to anonymous class in kotlin 96 | `val timer = object : CountDownTimer(...` 97 | 98 | 99 | ### 9 Lateinit, Nullability (lateinit-nullability) 100 | 101 | https://youtu.be/0N72ZisrwSE 102 | 103 | - Lateinit var property 104 | `lateinit var onFinishFunction: () -> Unit)` 105 | - Nullability in practice 106 | `private var onFinishFunction: (() -> Unit)? = null` 107 | - Function/Lambda as a property 108 | `lateinit var onTickFunction: (Long) -> Unit` 109 | - Function/Lambda as a method argument 110 | `fun onFinish(onFinishFunction: () -> Unit) {` 111 | - Invocation of Function/Lambda 112 | `onFinishFunction.invoke()` 113 | `onFinishFunction()` 114 | 115 | 116 | ### 10 Dagger Injections, Val Property (dagger-val-property) 117 | 118 | https://youtu.be/DXamJSAHyGQ 119 | 120 | - Adding new Dagger Module 121 | - Initializing component with two Modules 122 | - Creating Singleton object in Module 123 | - Val Property Lazy Initialization 124 | `private val ringtone by lazy {` 125 | ` RingtoneManager.getRingtone(context, RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))` 126 | `}` 127 | - Val Property with Getter 128 | `private val flags: Int` 129 | `get() = PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP` 130 | 131 | 132 | ### 11 Method with Default Arguments (default-arguments-method) 133 | 134 | https://youtu.be/mbXjX3xeOL0 135 | 136 | - Method with Default Arguments 137 | ` fun replaceFragment(fragment: Fragment, tag: String = fragment.javaClass.name, addToBackStack: Boolean = false) {` 138 | - Constructor with Default Arguments 139 | `class ContentFragmentManager(private val activity: FragmentActivity, private val contentLayoutId: Int = R.id.contentFrame) {` 140 | - Showing Fragment inside Activity 141 | - Creating `ContentFragmentManager` to replace fragments in `FrameLayout` 142 | 143 | 144 | ### 12 Abstract Property, Open Modifier (abstract-open-property) 145 | 146 | https://youtu.be/W08I-X7fWVw 147 | 148 | - Creating BaseFragment 149 | - Creating Abstract Property 150 | `protected abstract val layoutResourceId: Int` 151 | - Creating Open Property 152 | `open protected val menuResourceId: Int? = null` 153 | - Making 'Java Virtual method' final 154 | `final override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?)` 155 | 156 | 157 | ### 13 Custom Property Setter (custom-property-setter) 158 | 159 | https://youtu.be/xyzYjlaSkAs 160 | 161 | - Creating Custom Property Setter 162 | ` var delegate: Delegate? = null` 163 | ` set(value) {` 164 | ` field = value` 165 | ` notifyStateChanged()` 166 | ` }` 167 | - Creating Data Model 168 | - Creating Data Manager 169 | 170 | 171 | ### 14 String Templates (string-templates) 172 | 173 | https://youtu.be/jM3YmULqUcQ 174 | 175 | - String Template with value 176 | `timerText.text = "$value"` 177 | - String Template with expression 178 | `seriesNumberText.text = "${exerciseDataManager.exerciseData.seriesNumber}"` 179 | 180 | 181 | ### 15 Elvis Operator, Equality (elvis-operator-equality) 182 | 183 | https://youtu.be/Wn9-JarDvok 184 | 185 | - Using Elvis Operator 186 | `fragmentManager.fragments.firstOrNull { it?.isVisible ?: false }` 187 | - Using equality '==' sign correctly 188 | `if (currentFragment?.javaClass != fragment.javaClass) {` 189 | 190 | 191 | ### 16 Dagger Subcomponent, Custom Scope (dagger-subcomponent) 192 | 193 | https://youtu.be/xRRRujQly-0 194 | 195 | - Creating Dagger Module for specific Activity 196 | - Creating Dagger Subcomponent 197 | `@Subcomponent(modules = arrayOf(ExerciseModule::class))` 198 | - Creating Custom Scope to be used by Subcomponent 199 | `annotation class ActivityScope` 200 | 201 | 202 | ### 17 Anko Alert Dialog (anko-alert-dialog) 203 | 204 | https://youtu.be/qOxt4xOas2M 205 | 206 | - Creating Alert Dialog using Anko Library 207 | 208 | 209 | ## Useful links 210 | 211 | [Kotlin.org] (https://kotlinlang.org/) 212 | 213 | [Kotlink.Anko] (https://github.com/Kotlin/anko/) 214 | 215 | [Dagger] (https://github.com/google/dagger) 216 | 217 | 218 | ## Developed By 219 | 220 | Tomasz Morcinek 221 | tomasz.morcinek@gmail.com 222 | tomasz.morcinek.dev@gmail.com 223 | 224 | 225 | Follow me on Google+ 227 | 228 | 229 | Checkout my Applications in Google Play 231 | 232 | 233 | 234 | ## License 235 | 236 | You are free to do whatever you want with it. 237 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' 5 | 6 | android { 7 | compileSdkVersion 26 8 | buildToolsVersion "26.0.2" 9 | defaultConfig { 10 | applicationId "com.morcinek.workout" 11 | minSdkVersion 16 12 | targetSdkVersion 26 13 | versionCode 1 14 | versionName "1.0.0" 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | } 17 | signingConfigs { 18 | release { 19 | storeFile file(RELEASE_STORE_FILE) 20 | storePassword RELEASE_STORE_PASSWORD 21 | keyAlias RELEASE_KEY_ALIAS 22 | keyPassword RELEASE_KEY_PASSWORD 23 | } 24 | debug { 25 | storeFile file(DEBUG_STORE_FILE) 26 | storePassword DEBUG_STORE_PASSWORD 27 | keyAlias DEBUG_KEY_ALIAS 28 | keyPassword DEBUG_KEY_PASSWORD 29 | } 30 | } 31 | buildTypes { 32 | release { 33 | minifyEnabled false 34 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 35 | signingConfig signingConfigs.release 36 | } 37 | debug { 38 | applicationIdSuffix ".debug" 39 | } 40 | } 41 | lintOptions { 42 | checkReleaseBuilds false 43 | abortOnError false 44 | } 45 | packagingOptions { 46 | exclude 'META-INF/LICENSE' 47 | } 48 | applicationVariants.all { variant -> 49 | variant.buildConfigField 'String', 'DATE_FORMAT', '"dd/MM/yyyy"' 50 | variant.buildConfigField 'String', 'TIME_FORMAT', '"HH:mm"' 51 | variant.buildConfigField 'String', 'DAY_OF_WEEK_FORMAT', '"EEEE"' 52 | } 53 | } 54 | 55 | ext.versions = [ 56 | anko : '0.10.1', 57 | dagger : '2.12', 58 | support : '26.+', 59 | firebase: '11.6.2', 60 | ] 61 | 62 | dependencies { 63 | compile fileTree(dir: 'libs', include: ['*.jar']) 64 | compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 65 | compile "org.jetbrains.anko:anko:$versions.anko" 66 | compile "org.jetbrains.anko:anko-design:$versions.anko" 67 | 68 | compile "com.google.dagger:dagger:$versions.dagger" 69 | kapt "com.google.dagger:dagger-compiler:$versions.dagger" 70 | 71 | compile "com.android.support:appcompat-v7:$versions.support" 72 | compile "com.android.support:design:$versions.support" 73 | compile "com.android.support:preference-v14:$versions.support" 74 | compile "com.android.support:cardview-v7:$versions.support" 75 | compile 'com.android.support.constraint:constraint-layout:1.0.2' 76 | 77 | compile "com.google.firebase:firebase-core:$versions.firebase" 78 | compile "com.google.firebase:firebase-crash:$versions.firebase" 79 | compile "com.google.firebase:firebase-auth:$versions.firebase" 80 | compile "com.google.firebase:firebase-database:$versions.firebase" 81 | 82 | compile 'com.github.tmorcinek:kotlin-section-adapter:1.0.3' 83 | 84 | testCompile 'junit:junit:4.12' 85 | } 86 | 87 | apply plugin: 'com.google.gms.google-services' 88 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/tomaszmorcinek/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Workout-Debug 4 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 35 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/Application.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout 2 | 3 | import android.app.Application 4 | import com.morcinek.workout.common.di.ApplicationComponent 5 | import com.morcinek.workout.common.di.DaggerApplicationComponent 6 | import com.morcinek.workout.common.di.modules.AndroidModule 7 | import com.morcinek.workout.common.di.modules.ApplicationModule 8 | import com.morcinek.workout.core.data.DataModule 9 | import com.morcinek.workout.common.firebase.di.modules.FirebaseModule 10 | 11 | class Application : Application() { 12 | 13 | lateinit var component: ApplicationComponent 14 | 15 | override fun onCreate() { 16 | super.onCreate() 17 | component = DaggerApplicationComponent.builder() 18 | .androidModule(AndroidModule(this)) 19 | .applicationModule(ApplicationModule()) 20 | .firebaseModule(FirebaseModule()) 21 | .dataModule(DataModule()) 22 | .build() 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/FunctionalCountDownTimer.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common 2 | 3 | import android.os.CountDownTimer 4 | 5 | class FunctionalCountDownTimer(millisInFuture: Long, countDownInterval: Long) : CountDownTimer(millisInFuture, countDownInterval) { 6 | 7 | private var onFinishFunction: (() -> Unit)? = null 8 | private var onTickFunction: ((Long) -> Unit)? = null 9 | 10 | fun onFinish(onFinishFunction: () -> Unit) { 11 | this.onFinishFunction = onFinishFunction 12 | } 13 | 14 | fun onTick(onTickFunction: (Long) -> Unit) { 15 | this.onTickFunction = onTickFunction 16 | } 17 | 18 | override fun onFinish() { 19 | onFinishFunction?.invoke() 20 | } 21 | 22 | override fun onTick(millisUntilFinished: Long) { 23 | onTickFunction?.invoke(millisUntilFinished) 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/NotificationCenter.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import android.media.RingtoneManager 6 | import android.os.PowerManager 7 | import android.os.Vibrator 8 | import com.morcinek.workout.settings.soundEnabled 9 | import com.morcinek.workout.settings.vibrationEnabled 10 | import com.morcinek.workout.settings.vibrationValue 11 | 12 | class NotificationCenter(private val context: Context, private val vibrator: Vibrator, 13 | private val powerManager: PowerManager, private val sharedPreferences: SharedPreferences) { 14 | 15 | private val flags: Int 16 | get() = PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP 17 | 18 | private val ringtone by lazy { 19 | RingtoneManager.getRingtone(context, RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)) 20 | } 21 | 22 | fun sendNotifications() { 23 | if (sharedPreferences.soundEnabled) { 24 | ringtone.play() 25 | } 26 | 27 | if (sharedPreferences.vibrationEnabled) { 28 | vibrator.vibrate(vibrationTime()) 29 | } 30 | 31 | powerManager.newWakeLock(flags, javaClass.name).apply { 32 | acquire(1000) 33 | } 34 | } 35 | 36 | private fun vibrationTime() = sharedPreferences.vibrationValue * 200L 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/di/ActivityScope.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.di 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope 6 | @Retention 7 | annotation class ActivityScope -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/di/ApplicationComponent.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.di 2 | 3 | import com.morcinek.workout.common.di.modules.AndroidModule 4 | import com.morcinek.workout.common.di.modules.ApplicationModule 5 | import com.morcinek.workout.core.data.DataModule 6 | import com.morcinek.workout.common.firebase.di.modules.FirebaseModule 7 | import com.morcinek.workout.exercise.TimerService 8 | import com.morcinek.workout.exercise.di.ExerciseComponent 9 | import com.morcinek.workout.exercise.di.ExerciseModule 10 | import com.morcinek.workout.home.HomeActivity 11 | import com.morcinek.workout.home.exercises.ExercisesFragment 12 | import com.morcinek.workout.splash.SplashActivity 13 | import dagger.Component 14 | import javax.inject.Singleton 15 | 16 | @Singleton 17 | @Component(modules = arrayOf(AndroidModule::class, ApplicationModule::class, FirebaseModule::class, DataModule::class)) 18 | interface ApplicationComponent { 19 | 20 | fun inject(activity: HomeActivity) 21 | fun inject(activity: SplashActivity) 22 | 23 | fun inject(fragment: ExercisesFragment) 24 | 25 | fun inject(service: TimerService) 26 | 27 | fun add(exerciseModule: ExerciseModule): ExerciseComponent 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/di/DaggerUtils.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.di 2 | 3 | import android.app.Service 4 | import android.support.v4.app.Fragment 5 | import android.support.v7.app.AppCompatActivity 6 | import com.morcinek.workout.Application 7 | 8 | val AppCompatActivity.component: ApplicationComponent 9 | get() = (application as Application).component 10 | 11 | val Fragment.component: ApplicationComponent 12 | get() = (activity.application as Application).component 13 | 14 | val Service.component: ApplicationComponent 15 | get() = (application as Application).component 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/di/modules/AndroidModule.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.di.modules 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.os.PowerManager 6 | import android.os.Vibrator 7 | import dagger.Module 8 | import dagger.Provides 9 | import org.jetbrains.anko.defaultSharedPreferences 10 | import javax.inject.Singleton 11 | 12 | @Module 13 | class AndroidModule(private val application: Application) { 14 | 15 | @Provides 16 | fun provideApplicationContext(): Context = application 17 | 18 | @Provides 19 | @Singleton 20 | fun providePowerManager() = application.getSystemService(Context.POWER_SERVICE) as PowerManager 21 | 22 | @Provides 23 | @Singleton 24 | fun provideVibrator() = application.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator 25 | 26 | @Provides 27 | @Singleton 28 | fun provideSharedPreferences() = application.defaultSharedPreferences 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/di/modules/ApplicationModule.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.di.modules 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import android.os.PowerManager 6 | import android.os.Vibrator 7 | import com.morcinek.workout.common.NotificationCenter 8 | import dagger.Module 9 | import dagger.Provides 10 | 11 | @Module 12 | class ApplicationModule() { 13 | 14 | @Provides 15 | fun provideNotificationCenter(context: Context, vibrator: Vibrator, powerManager: PowerManager, sharedPreferences: SharedPreferences) 16 | = NotificationCenter(context, vibrator, powerManager, sharedPreferences) 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/firebase/auth/UserManager.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.firebase.auth 2 | 3 | import com.google.firebase.auth.AuthCredential 4 | import com.google.firebase.auth.FirebaseAuth 5 | import com.google.firebase.auth.FirebaseUser 6 | 7 | class UserManager(private val firebaseAuth: FirebaseAuth) { 8 | 9 | val currentUser: FirebaseUser? 10 | get() = firebaseAuth.currentUser 11 | 12 | val isUserAuthenticated: Boolean 13 | get() = currentUser != null 14 | 15 | fun signInAnonymously() = firebaseAuth.signInAnonymously() 16 | 17 | fun connectWithCredentials(authCredential: AuthCredential) = 18 | if (isUserAuthenticated) { 19 | currentUser!!.linkWithCredential(authCredential) 20 | } else { 21 | firebaseAuth.signInWithCredential(authCredential) 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/firebase/data/DataManager.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.firebase.data 2 | 3 | import com.google.firebase.database.DatabaseReference 4 | 5 | abstract class DataManager { 6 | 7 | protected abstract val reference: DatabaseReference 8 | 9 | var key: String? = null 10 | 11 | private val objectReference by lazy { if (key != null) reference.child(key) else reference.push() } 12 | 13 | fun update(data: DataModel) = objectReference.updateChildren(data.toMap()) 14 | 15 | fun remove() = objectReference.removeValue() 16 | 17 | fun get() = objectReference 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/firebase/data/DataManagerImpl.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.firebase.data 2 | 3 | import com.google.firebase.database.DatabaseReference 4 | 5 | open class DataManagerImpl(override val reference: DatabaseReference) : DataManager() -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/firebase/data/DataModel.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.firebase.data 2 | 3 | interface DataModel { 4 | 5 | fun toMap(): Map 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/firebase/data/DataProvider.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.firebase.data 2 | 3 | import com.google.firebase.database.* 4 | 5 | abstract class DataProvider : ValueEventListener { 6 | 7 | lateinit var delegate: Delegate 8 | 9 | protected abstract val reference: Query 10 | protected abstract val type: Class 11 | 12 | fun register() { 13 | reference.addValueEventListener(this) 14 | } 15 | 16 | fun registerForSingleValue() { 17 | reference.addListenerForSingleValueEvent(this) 18 | } 19 | 20 | fun unregister() { 21 | reference.removeEventListener(this) 22 | } 23 | 24 | override fun onDataChange(dataSnapshot: DataSnapshot?) { 25 | delegate.success(dataSnapshot!!.children.map { it.key to it.getValue(type)!! }) 26 | } 27 | 28 | override fun onCancelled(error: DatabaseError?) { 29 | delegate.failed(error!!.message) 30 | } 31 | 32 | interface Delegate { 33 | fun success(values: List>) 34 | fun failed(errorMessage: String) 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/firebase/data/InteractorDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.firebase.data 2 | 3 | interface InteractorDelegate { 4 | fun success(values: T) 5 | fun failed(errorMessage: String) 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/firebase/database/ReferenceManager.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.firebase.database 2 | 3 | import com.google.firebase.auth.FirebaseAuth 4 | import com.google.firebase.database.FirebaseDatabase 5 | 6 | class ReferenceManager(val firebaseAuth: FirebaseAuth, val firebaseDatabase: FirebaseDatabase) { 7 | 8 | init { 9 | firebaseDatabase.setPersistenceEnabled(true) 10 | } 11 | 12 | private val userId: String 13 | get() = firebaseAuth.currentUser!!.uid 14 | 15 | fun exercises() = firebaseDatabase.getReference("data") 16 | .child(userId) 17 | .child("exercises") 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/firebase/di/modules/FirebaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.firebase.di.modules 2 | 3 | import com.google.firebase.auth.FirebaseAuth 4 | import com.google.firebase.database.FirebaseDatabase 5 | import com.morcinek.workout.common.di.modules.AndroidModule 6 | import com.morcinek.workout.common.firebase.auth.UserManager 7 | import com.morcinek.workout.common.firebase.database.ReferenceManager 8 | import dagger.Module 9 | import dagger.Provides 10 | import javax.inject.Singleton 11 | 12 | @Module(includes = arrayOf(AndroidModule::class)) 13 | class FirebaseModule { 14 | 15 | @Provides 16 | @Singleton 17 | fun provideFirebaseDatabase() = FirebaseDatabase.getInstance() 18 | 19 | @Provides 20 | @Singleton 21 | fun provideFirebaseAuth() = FirebaseAuth.getInstance() 22 | 23 | @Provides 24 | fun provideUserManager(firebaseAuth: FirebaseAuth) = UserManager(firebaseAuth) 25 | 26 | @Provides 27 | @Singleton 28 | fun provideFirebaseDataManager(firebaseAuth: FirebaseAuth, firebaseDatabase: FirebaseDatabase) 29 | = ReferenceManager(firebaseAuth, firebaseDatabase) 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/fragment/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.fragment 2 | 3 | import android.os.Bundle 4 | import android.support.v4.app.Fragment 5 | import android.view.* 6 | 7 | abstract class BaseFragment : Fragment() { 8 | 9 | protected abstract val layoutResourceId: Int 10 | 11 | open protected val menuResourceId: Int? = null 12 | 13 | private val hasMenu by lazy { menuResourceId != null } 14 | 15 | final override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?) 16 | = inflater?.inflate(layoutResourceId, container, false) 17 | 18 | override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { 19 | super.onViewCreated(view, savedInstanceState) 20 | setHasOptionsMenu(hasMenu) 21 | } 22 | 23 | override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { 24 | if (hasMenu) { 25 | inflater?.inflate(menuResourceId!!, menu) 26 | } 27 | super.onCreateOptionsMenu(menu, inflater) 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/fragment/ContentFragmentManager.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.fragment 2 | 3 | import android.support.v4.app.Fragment 4 | import android.support.v4.app.FragmentActivity 5 | import com.morcinek.workout.R 6 | 7 | class ContentFragmentManager(private val activity: FragmentActivity, private val contentLayoutId: Int = R.id.contentFrame) { 8 | 9 | private val fragmentManager by lazy { activity.supportFragmentManager } 10 | 11 | val currentFragment: Fragment? 12 | get() = fragmentManager.fragments.firstOrNull { it?.isVisible ?: false } 13 | 14 | fun replaceFragment(fragment: Fragment, tag: String = fragment.javaClass.name, addToBackStack: Boolean = false) { 15 | if (currentFragment?.javaClass != fragment.javaClass) { 16 | val fragmentTransaction = fragmentManager.beginTransaction() 17 | fragmentTransaction.replace(contentLayoutId, fragment, tag) 18 | if (addToBackStack) { 19 | fragmentTransaction.addToBackStack(tag) 20 | } 21 | fragmentTransaction.commit() 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/fragment/SingleFragmentActivity.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.fragment 2 | 3 | import android.os.Bundle 4 | import android.support.v4.app.Fragment 5 | import android.support.v7.app.AppCompatActivity 6 | import android.view.MenuItem 7 | import com.morcinek.workout.R 8 | import com.morcinek.workout.common.utils.getSerializableExtra 9 | import kotlinx.android.synthetic.main.content_fragment.* 10 | 11 | class SingleFragmentActivity : AppCompatActivity() { 12 | 13 | override fun onCreate(savedInstanceState: Bundle?) { 14 | super.onCreate(savedInstanceState) 15 | // overridePendingTransition(R.anim.slide_in_left, R.anim.slide_outside_right) 16 | setContentView(R.layout.content_fragment) 17 | setupToolbar() 18 | if (savedInstanceState == null) { 19 | val classType = intent.getSerializableExtra>()!! 20 | supportFragmentManager.beginTransaction().replace(R.id.contentFrame, classType.newInstance()).commit(); 21 | } 22 | } 23 | 24 | private fun setupToolbar() { 25 | setSupportActionBar(toolbar) 26 | supportActionBar!!.setDisplayHomeAsUpEnabled(true) 27 | } 28 | 29 | override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { 30 | android.R.id.home -> { 31 | onBackPressed() 32 | true 33 | } 34 | else -> super.onOptionsItemSelected(item) 35 | } 36 | 37 | override fun finish() { 38 | super.finish() 39 | // overridePendingTransition(R.anim.slide_inside_left, R.anim.slide_out_right) 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/preference/TimePreference.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.preference 2 | 3 | import android.content.Context 4 | import android.content.res.TypedArray 5 | import android.support.v7.preference.DialogPreference 6 | import android.util.AttributeSet 7 | import com.morcinek.workout.R 8 | 9 | class TimePreference(context: Context?, attrs: AttributeSet?) : DialogPreference(context, attrs) { 10 | 11 | var time: Int = 0 12 | set(value) { 13 | field = value 14 | persistInt(time) 15 | } 16 | 17 | override fun getDialogLayoutResource() = R.layout.pref_dialog_time 18 | 19 | override fun onGetDefaultValue(a: TypedArray?, index: Int): Any? = a?.getInt(index, 0) 20 | 21 | override fun onSetInitialValue(restorePersistedValue: Boolean, defaultValue: Any?) { 22 | time = if (restorePersistedValue) getPersistedInt(time) else defaultValue as Int 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/preference/TimePreferenceDialogFragmentCompat.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.preference 2 | 3 | import android.support.v7.preference.PreferenceDialogFragmentCompat 4 | import android.view.View 5 | import android.widget.NumberPicker 6 | import com.morcinek.workout.R 7 | 8 | const val ARG_KEY = "key" 9 | const val ARG_MIN = "min" 10 | const val ARG_MAX = "max" 11 | const val ARG_INCREMENT = "increment" 12 | 13 | class TimePreferenceDialogFragmentCompat : PreferenceDialogFragmentCompat() { 14 | 15 | private lateinit var mTimePicker: NumberPicker 16 | 17 | private val max by lazy { arguments[ARG_MAX] as Int } 18 | private val min by lazy { arguments[ARG_MIN] as Int } 19 | private val increment by lazy { arguments[ARG_INCREMENT] as Int } 20 | 21 | private val timePreference: TimePreference 22 | get() = preference as TimePreference 23 | 24 | override fun onBindDialogView(view: View?) { 25 | super.onBindDialogView(view) 26 | 27 | mTimePicker = view!!.findViewById(R.id.edit) 28 | 29 | val minValue = 0 30 | val maxValue = (max - min) / increment 31 | 32 | mTimePicker.minValue = minValue 33 | mTimePicker.maxValue = maxValue 34 | mTimePicker.displayedValues = (minValue..maxValue).map { min + it * increment } 35 | .map { "$it" } 36 | .toTypedArray() 37 | 38 | mTimePicker.value = getPickerValue(timePreference.time) 39 | } 40 | 41 | private fun getReturnValue(localValue: Int) = localValue * increment + min 42 | 43 | private fun getPickerValue(returnValue: Int) = (returnValue - min) / increment 44 | 45 | override fun onDialogClosed(positiveResult: Boolean) { 46 | if (positiveResult) { 47 | val value = mTimePicker.value 48 | if (preference.callChangeListener(value)) { 49 | timePreference.time = getReturnValue(value) 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/utils/AndroidUtils.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.utils 2 | 3 | import android.app.Activity 4 | import android.app.Service 5 | import android.content.BroadcastReceiver 6 | import android.content.Context 7 | import android.content.Intent 8 | 9 | inline fun Activity.stopService() { 10 | stopService(Intent(this, T::class.java)) 11 | } 12 | 13 | inline fun broadcastReceiver(crossinline onReceiveFunction: (Intent) -> Unit) = object : BroadcastReceiver() { 14 | override fun onReceive(context: Context, intent: Intent) = onReceiveFunction(intent) 15 | } 16 | 17 | fun handleOptionItemAction(function: () -> Any): Boolean { 18 | function() 19 | return true 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/utils/CalendarUtils.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.utils 2 | 3 | import com.morcinek.workout.BuildConfig 4 | import java.text.SimpleDateFormat 5 | import java.util.* 6 | 7 | /** 8 | * Copyright 2016 Tomasz Morcinek. All rights reserved. 9 | */ 10 | 11 | fun Calendar.formatWith(dateFormat: SimpleDateFormat) = dateFormat.format(time) 12 | 13 | fun dateFormat() = SimpleDateFormat(BuildConfig.DATE_FORMAT) 14 | fun timeFormat() = SimpleDateFormat(BuildConfig.TIME_FORMAT) 15 | fun dayOfWeekFormat() = SimpleDateFormat(BuildConfig.DAY_OF_WEEK_FORMAT) 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/utils/FragmentUtils.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.utils 2 | 3 | import android.support.v7.app.AppCompatActivity 4 | 5 | fun android.support.v4.app.Fragment.setTitle(resourceId: Int) { 6 | (activity as AppCompatActivity).supportActionBar!!.setTitle(resourceId) 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/utils/IntentUtils.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.utils 2 | 3 | import android.content.Intent 4 | import android.os.Parcelable 5 | import java.io.Serializable 6 | 7 | inline fun Intent.getSerializableExtra() = getSerializableExtra(T::class.java.name) as T? 8 | 9 | fun Intent.putParcelableExtra(value: T) = putExtra(value.javaClass.getName(), value) 10 | fun Intent.putSerializableExtra(value: T) = putExtra(value.javaClass.getName(), value) 11 | 12 | fun Intent.getString() = getStringExtra(String::class.java.name) 13 | fun Intent.put(value: String) = putExtra(String::class.java.name, value) 14 | 15 | fun Intent.getFloat() = getFloatExtra(Float::class.java.name, 0F) 16 | fun Intent.put(value: Float) = putExtra(Float::class.java.name, value) 17 | 18 | fun Intent.getLong() = getLongExtra(Long::class.java.name, 0L) 19 | fun Intent.put(value: Long) = putExtra(Long::class.java.name, value) 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/utils/SharedPreferencesUtils.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.utils 2 | 3 | import android.content.SharedPreferences 4 | 5 | fun SharedPreferences.getInt(preferenceKey: String) = getInt(preferenceKey, 0) 6 | fun SharedPreferences.getIntOrNull(preferenceKey: String) = if (contains(preferenceKey)) getInt(preferenceKey) else null 7 | 8 | fun SharedPreferences.getLong(preferenceKey: String) = getLong(preferenceKey, 0) 9 | fun SharedPreferences.getLongOrNull(preferenceKey: String) = if (contains(preferenceKey)) getLong(preferenceKey, 0) else null 10 | 11 | fun SharedPreferences.getBoolean(preferenceKey: String) = getBoolean(preferenceKey, false) 12 | fun SharedPreferences.getBooleanOrNull(preferenceKey: String) = if (contains(preferenceKey)) getBoolean(preferenceKey, false) else null 13 | 14 | fun SharedPreferences.commitFunction(function: SharedPreferences.Editor.() -> SharedPreferences.Editor) = edit().function().commit() 15 | 16 | fun SharedPreferences.applyFunction(function: SharedPreferences.Editor.() -> SharedPreferences.Editor) = edit().function().apply() 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/common/utils/StartActivityUtils.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.common.utils 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import org.jetbrains.anko.internals.AnkoInternals 7 | 8 | inline fun Context.startActivity(vararg params: Any) { 9 | val arrayOfPairs = params.map { Pair(it.javaClass.name, it) }.toTypedArray() 10 | AnkoInternals.internalStartActivity(this, T::class.java, arrayOfPairs) 11 | } 12 | 13 | inline fun Activity.startActivityFun(function: Intent.() -> Any) { 14 | val intent = AnkoInternals.createIntent(this, T::class.java, emptyArray()) 15 | function(intent) 16 | startActivity(intent) 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/core/data/DataModule.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.core.data 2 | 3 | import android.content.Context 4 | import com.morcinek.workout.common.firebase.database.ReferenceManager 5 | import com.morcinek.workout.common.firebase.di.modules.FirebaseModule 6 | import com.morcinek.workout.core.data.exercises.ExercisesProvider 7 | import com.morcinek.workout.home.exercises.ExercisesInteractor 8 | import dagger.Module 9 | import dagger.Provides 10 | 11 | @Module(includes = arrayOf(FirebaseModule::class)) 12 | class DataModule { 13 | 14 | @Provides 15 | fun provideExercisesProvider(referenceManager: ReferenceManager) = ExercisesProvider(referenceManager) 16 | 17 | @Provides 18 | fun provideExercisesInteractor(context: Context, exercisesProvider: ExercisesProvider) = ExercisesInteractor(context, exercisesProvider) 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/core/data/DataUtils.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.core.data 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.os.Parcelable 6 | import com.morcinek.workout.common.utils.putParcelableExtra 7 | 8 | fun Intent.putKeyExtra(value: String) = putExtra("key", value) 9 | fun Intent.getKeyExtra() : String? = getStringExtra("key") 10 | 11 | fun Bundle.putKey(value: String) = putString("key", value) 12 | 13 | inline fun Intent.putPair(pair: Pair) { 14 | putKeyExtra(pair.first) 15 | putParcelableExtra(pair.second) 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/core/data/exercises/ExerciseDataModel.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.core.data.exercises 2 | 3 | import com.google.firebase.database.Exclude 4 | import com.google.firebase.database.IgnoreExtraProperties 5 | import com.morcinek.workout.common.firebase.data.DataModel 6 | import java.util.* 7 | 8 | @IgnoreExtraProperties 9 | class ExerciseDataModel(var name: String = "", var category: String = "", var timeInMillis: Long = Calendar.getInstance().timeInMillis, var numberOfSeries: Int = 1) : DataModel { 10 | 11 | @Exclude 12 | override fun toMap() = mapOf( 13 | "name" to name, 14 | "category" to category, 15 | "timeInMillis" to timeInMillis, 16 | "numberOfSeries" to numberOfSeries 17 | ) 18 | 19 | var date: Calendar 20 | @Exclude get() = Calendar.getInstance().apply { timeInMillis = this@ExerciseDataModel.timeInMillis } 21 | @Exclude set(value) { 22 | timeInMillis = value.timeInMillis 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/core/data/exercises/ExerciseManager.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.core.data.exercises 2 | 3 | import com.morcinek.workout.common.firebase.data.DataManagerImpl 4 | import com.morcinek.workout.common.firebase.database.ReferenceManager 5 | 6 | class ExerciseManager(referenceManager: ReferenceManager) : DataManagerImpl(referenceManager.exercises()){ 7 | 8 | fun setName(name: String) = get().child("name").setValue(name) 9 | fun setCategory(category: String) = get().child("category").setValue(category) 10 | fun setNumberOfSeries(numberOfSeries: Int) = get().child("numberOfSeries").setValue(numberOfSeries) 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/core/data/exercises/ExercisesProvider.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.core.data.exercises 2 | 3 | import com.morcinek.workout.common.firebase.data.DataProvider 4 | import com.morcinek.workout.common.firebase.database.ReferenceManager 5 | 6 | class ExercisesProvider(private val referenceManager: ReferenceManager) : DataProvider() { 7 | 8 | override val type = ExerciseDataModel::class.java 9 | 10 | override val reference by lazy { referenceManager.exercises() } 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/exercise/ExerciseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.exercise 2 | 3 | import android.content.IntentFilter 4 | import android.os.Bundle 5 | import android.support.v7.app.AppCompatActivity 6 | import android.view.Menu 7 | import android.view.MenuItem 8 | import com.morcinek.workout.R 9 | import com.morcinek.workout.common.NotificationCenter 10 | import com.morcinek.workout.common.di.component 11 | import com.morcinek.workout.common.fragment.BaseFragment 12 | import com.morcinek.workout.common.fragment.ContentFragmentManager 13 | import com.morcinek.workout.common.fragment.SingleFragmentActivity 14 | import com.morcinek.workout.common.utils.* 15 | import com.morcinek.workout.core.data.exercises.ExerciseManager 16 | import com.morcinek.workout.core.data.getKeyExtra 17 | import com.morcinek.workout.exercise.di.ExerciseComponent 18 | import com.morcinek.workout.exercise.di.ExerciseModule 19 | import com.morcinek.workout.exercise.fragments.* 20 | import com.morcinek.workout.settings.SettingsFragment 21 | import kotlinx.android.synthetic.main.content_fragment.* 22 | import org.jetbrains.anko.alert 23 | import javax.inject.Inject 24 | 25 | class ExerciseActivity : AppCompatActivity(), ExerciseDataManager.Delegate { 26 | 27 | @Inject lateinit var contentFragmentManager: ContentFragmentManager 28 | 29 | @Inject lateinit var exerciseDataManager: ExerciseDataManager 30 | @Inject lateinit var exerciseManager: ExerciseManager 31 | @Inject lateinit var notificationCenter: NotificationCenter 32 | 33 | val key by lazy { intent.getKeyExtra() } 34 | 35 | val exerciseComponent by lazy { component.add(ExerciseModule(this)) } 36 | 37 | override fun onCreate(savedInstanceState: Bundle?) { 38 | super.onCreate(savedInstanceState) 39 | setContentView(R.layout.content_fragment) 40 | exerciseComponent.inject(this) 41 | setupToolbar() 42 | registerReceiver(timerReceiver, IntentFilter(TIMER_SERVICE_FINISH)) 43 | } 44 | 45 | override fun onDestroy() { 46 | super.onDestroy() 47 | unregisterReceiver(timerReceiver) 48 | stopService() 49 | } 50 | 51 | private fun setupToolbar() { 52 | setSupportActionBar(toolbar) 53 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 54 | } 55 | 56 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 57 | menuInflater.inflate(R.menu.exercise, menu) 58 | return true 59 | } 60 | 61 | override fun onPrepareOptionsMenu(menu: Menu?): Boolean { 62 | menu?.findItem(R.id.action_edit)?.isVisible = exerciseDataManager.isEditable 63 | return super.onPrepareOptionsMenu(menu) 64 | } 65 | 66 | override fun onStateChanged(exerciseState: ExerciseState) { 67 | contentFragmentManager.replaceFragment(when (exerciseState) { 68 | ExerciseState.Series -> SeriesFragment() 69 | ExerciseState.Break -> BreakFragment() 70 | ExerciseState.Splash -> BreakSplashFragment() 71 | ExerciseState.New -> NewFragment() 72 | ExerciseState.Loading -> LoadingFragment() 73 | }) 74 | invalidateOptionsMenu() 75 | } 76 | 77 | override fun onResume() { 78 | super.onResume() 79 | exerciseDataManager.delegate = this 80 | } 81 | 82 | override fun onPause() { 83 | super.onPause() 84 | exerciseDataManager.delegate = null 85 | } 86 | 87 | private val timerReceiver = broadcastReceiver { 88 | notificationCenter.sendNotifications() 89 | exerciseDataManager.incrementSeriesNumber() 90 | exerciseDataManager.showBreakSplash() 91 | } 92 | 93 | override fun onOptionsItemSelected(item: MenuItem?) = when (item?.itemId) { 94 | android.R.id.home -> handleOptionItemAction { onBackPressed() } 95 | R.id.action_edit -> handleOptionItemAction { exerciseDataManager.showNew() } 96 | R.id.action_delete -> handleOptionItemAction { 97 | alert(R.string.exercise_exit_message) { 98 | positiveButton(R.string.yes) { finishWithDelete() } 99 | negativeButton(R.string.no) { } 100 | }.show() 101 | } 102 | R.id.action_settings -> handleOptionItemAction { 103 | startActivityFun { putSerializableExtra(SettingsFragment::class.java) } 104 | } 105 | 106 | else -> super.onOptionsItemSelected(item) 107 | } 108 | 109 | 110 | override fun onBackPressed() { 111 | if (key == null) { 112 | alert(R.string.exercise_exit_message) { 113 | positiveButton(R.string.yes) { saveAndFinish() } 114 | negativeButton(R.string.no) { finishWithDelete() } 115 | }.show() 116 | } else { 117 | saveAndFinish() 118 | } 119 | } 120 | 121 | private fun finishWithDelete() { 122 | exerciseManager.remove() 123 | finish() 124 | } 125 | 126 | private fun saveAndFinish() { 127 | exerciseManager.update(exerciseDataManager.exerciseDataModel) 128 | finish() 129 | } 130 | } 131 | 132 | inline val BaseFragment.exerciseComponent: ExerciseComponent 133 | get() = (activity as ExerciseActivity).exerciseComponent 134 | -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/exercise/ExerciseDataManager.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.exercise 2 | 3 | import android.content.SharedPreferences 4 | import com.morcinek.workout.core.data.exercises.ExerciseDataModel 5 | import com.morcinek.workout.settings.breakTime 6 | 7 | class ExerciseDataManager(private val sharedPreferences: SharedPreferences) { 8 | 9 | var exerciseState: ExerciseState = ExerciseState.Loading 10 | 11 | lateinit var exerciseDataModel: ExerciseDataModel 12 | 13 | var numberOfSeries: Int 14 | get() = exerciseDataModel.numberOfSeries 15 | set(value) { 16 | exerciseDataModel.numberOfSeries = value 17 | } 18 | 19 | val breakIntervalSeconds: Long 20 | get() = sharedPreferences.breakTime.toLong() 21 | 22 | var delegate: Delegate? = null 23 | set(value) { 24 | field = value 25 | notifyStateChanged() 26 | } 27 | 28 | fun incrementSeriesNumber() { 29 | numberOfSeries += 1 30 | } 31 | 32 | fun showNew() { 33 | exerciseState = ExerciseState.New 34 | notifyStateChanged() 35 | } 36 | 37 | fun showBreak() { 38 | exerciseState = ExerciseState.Break 39 | notifyStateChanged() 40 | } 41 | 42 | fun showBreakSplash() { 43 | exerciseState = ExerciseState.Splash 44 | notifyStateChanged() 45 | } 46 | 47 | fun showSeries() { 48 | exerciseState = ExerciseState.Series 49 | notifyStateChanged() 50 | } 51 | 52 | private fun notifyStateChanged() { 53 | delegate?.onStateChanged(exerciseState) 54 | } 55 | 56 | interface Delegate { 57 | fun onStateChanged(exerciseState: ExerciseState) 58 | } 59 | 60 | val isEditable: Boolean 61 | get() = exerciseState != ExerciseState.New 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/exercise/ExerciseState.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.exercise 2 | 3 | enum class ExerciseState { 4 | Series, Break, Splash, New, Loading 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/exercise/TimerService.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.exercise 2 | 3 | import android.app.Service 4 | import android.content.Intent 5 | import android.content.SharedPreferences 6 | import android.text.format.DateUtils 7 | import com.morcinek.workout.common.FunctionalCountDownTimer 8 | import com.morcinek.workout.common.di.component 9 | import com.morcinek.workout.common.utils.put 10 | import com.morcinek.workout.settings.breakTime 11 | import javax.inject.Inject 12 | 13 | 14 | val TIMER_SERVICE_TICK = "com.morcinek.workout.TimerService.Tick" 15 | val TIMER_SERVICE_FINISH = "com.morcinek.workout.TimerService.Finish" 16 | 17 | class TimerService : Service() { 18 | 19 | @Inject lateinit var sharedPreferences: SharedPreferences 20 | 21 | private val breakIntervalInMillis: Long 22 | get() = sharedPreferences.breakTime * DateUtils.SECOND_IN_MILLIS 23 | 24 | private var timer : FunctionalCountDownTimer? = null 25 | 26 | override fun onCreate() { 27 | super.onCreate() 28 | component.inject(this) 29 | } 30 | 31 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 32 | if (timer == null) { 33 | timer = FunctionalCountDownTimer(breakIntervalInMillis, 100).apply { 34 | onTick { sendBroadcast(Intent(TIMER_SERVICE_TICK).apply { put(it) }) } 35 | onFinish { 36 | sendBroadcast(Intent(TIMER_SERVICE_FINISH)) 37 | timer = null 38 | } 39 | start() 40 | } 41 | } 42 | return super.onStartCommand(intent, flags, startId) 43 | } 44 | 45 | override fun onDestroy() { 46 | timer?.cancel() 47 | super.onDestroy() 48 | } 49 | 50 | override fun onBind(intent: Intent?) = null 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/exercise/di/ExerciseComponent.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.exercise.di 2 | 3 | import com.morcinek.workout.common.di.ActivityScope 4 | import com.morcinek.workout.exercise.ExerciseActivity 5 | import com.morcinek.workout.exercise.fragments.* 6 | import dagger.Subcomponent 7 | 8 | @ActivityScope 9 | @Subcomponent(modules = arrayOf(ExerciseModule::class)) 10 | interface ExerciseComponent { 11 | 12 | fun inject(activity: ExerciseActivity) 13 | 14 | fun inject(fragment: SeriesFragment) 15 | fun inject(fragment: BreakFragment) 16 | fun inject(fragment: BreakSplashFragment) 17 | fun inject(fragment: LoadingFragment) 18 | fun inject(fragment: NewFragment) 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/exercise/di/ExerciseModule.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.exercise.di 2 | 3 | import android.content.SharedPreferences 4 | import android.support.v4.app.FragmentActivity 5 | import com.morcinek.workout.common.di.ActivityScope 6 | import com.morcinek.workout.common.firebase.database.ReferenceManager 7 | import com.morcinek.workout.common.fragment.ContentFragmentManager 8 | import com.morcinek.workout.core.data.exercises.ExerciseManager 9 | import com.morcinek.workout.exercise.ExerciseDataManager 10 | import dagger.Module 11 | import dagger.Provides 12 | 13 | @Module 14 | class ExerciseModule(private val activity: FragmentActivity) { 15 | 16 | @Provides 17 | @ActivityScope 18 | fun provideExerciseDataManager(sharedPreferences: SharedPreferences) = ExerciseDataManager(sharedPreferences) 19 | 20 | @Provides 21 | @ActivityScope 22 | fun provideExerciseManager(referenceManager: ReferenceManager) = ExerciseManager(referenceManager) 23 | 24 | @Provides 25 | fun provideContentFragmentManager() = ContentFragmentManager(activity) 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/exercise/fragments/BreakFragment.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.exercise.fragments 2 | 3 | import android.content.IntentFilter 4 | import android.os.Bundle 5 | import android.text.format.DateUtils 6 | import android.view.View 7 | import com.morcinek.workout.R 8 | import com.morcinek.workout.common.fragment.BaseFragment 9 | import com.morcinek.workout.common.utils.broadcastReceiver 10 | import com.morcinek.workout.common.utils.getLong 11 | import com.morcinek.workout.common.utils.stopService 12 | import com.morcinek.workout.exercise.ExerciseDataManager 13 | import com.morcinek.workout.exercise.TIMER_SERVICE_TICK 14 | import com.morcinek.workout.exercise.TimerService 15 | import com.morcinek.workout.exercise.exerciseComponent 16 | import kotlinx.android.synthetic.main.exercise_break.* 17 | import org.jetbrains.anko.support.v4.startService 18 | import javax.inject.Inject 19 | 20 | 21 | class BreakFragment : BaseFragment() { 22 | 23 | override val layoutResourceId = R.layout.exercise_break 24 | 25 | @Inject lateinit var exerciseDataManager: ExerciseDataManager 26 | 27 | private val breakIntervalInMillis: Long 28 | get() = exerciseDataManager.breakIntervalSeconds * DateUtils.SECOND_IN_MILLIS 29 | 30 | override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { 31 | super.onViewCreated(view, savedInstanceState) 32 | exerciseComponent.inject(this) 33 | 34 | seriesNumberText.text = "${exerciseDataManager.numberOfSeries}" 35 | startNewSeriesButton.setOnClickListener { 36 | exerciseDataManager.incrementSeriesNumber() 37 | exerciseDataManager.showSeries() 38 | } 39 | cancelButton.setOnClickListener { 40 | activity.stopService() 41 | exerciseDataManager.showSeries() 42 | } 43 | progressBar.max = breakIntervalInMillis.toInt() 44 | startService() 45 | } 46 | 47 | override fun onResume() { 48 | super.onResume() 49 | activity.registerReceiver(broadcastReceiver, IntentFilter(TIMER_SERVICE_TICK)) 50 | } 51 | 52 | override fun onPause() { 53 | super.onPause() 54 | activity.unregisterReceiver(broadcastReceiver) 55 | } 56 | 57 | private fun updateProgress(progress: Long) { 58 | timerText.text = "${progress / 1000}.${progress % 1000 / 100}" 59 | progressBar.progress = (breakIntervalInMillis - progress).toInt() 60 | } 61 | 62 | private val broadcastReceiver = broadcastReceiver { 63 | updateProgress(it.getLong()) 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/exercise/fragments/BreakSplashFragment.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.exercise.fragments 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import com.morcinek.workout.R 6 | import com.morcinek.workout.common.fragment.BaseFragment 7 | import com.morcinek.workout.exercise.ExerciseDataManager 8 | import com.morcinek.workout.exercise.exerciseComponent 9 | import kotlinx.android.synthetic.main.exercise_break_splash.* 10 | import javax.inject.Inject 11 | 12 | class BreakSplashFragment : BaseFragment() { 13 | 14 | override val layoutResourceId = R.layout.exercise_break_splash 15 | 16 | @Inject lateinit var exerciseDataManager: ExerciseDataManager 17 | 18 | override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { 19 | super.onViewCreated(view, savedInstanceState) 20 | exerciseComponent.inject(this) 21 | 22 | breakSplashText.text = getString(R.string.break_splash_series_text, exerciseDataManager.numberOfSeries) 23 | view?.setOnClickListener { 24 | exerciseDataManager.showSeries() 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/exercise/fragments/LoadingFragment.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.exercise.fragments 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import com.google.firebase.database.DataSnapshot 6 | import com.google.firebase.database.DatabaseError 7 | import com.google.firebase.database.ValueEventListener 8 | import com.morcinek.workout.R 9 | import com.morcinek.workout.common.fragment.BaseFragment 10 | import com.morcinek.workout.core.data.exercises.ExerciseDataModel 11 | import com.morcinek.workout.core.data.exercises.ExerciseManager 12 | import com.morcinek.workout.core.data.getKeyExtra 13 | import com.morcinek.workout.exercise.ExerciseDataManager 14 | import com.morcinek.workout.exercise.exerciseComponent 15 | import org.jetbrains.anko.design.snackbar 16 | import javax.inject.Inject 17 | 18 | class LoadingFragment : BaseFragment(), ValueEventListener { 19 | 20 | override val layoutResourceId = R.layout.exercise_loading 21 | 22 | @Inject lateinit var exerciseDataManager: ExerciseDataManager 23 | @Inject lateinit var exerciseManager: ExerciseManager 24 | 25 | val key by lazy { activity.intent.getKeyExtra() } 26 | 27 | override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { 28 | super.onViewCreated(view, savedInstanceState) 29 | exerciseComponent.inject(this) 30 | 31 | if (key == null) { 32 | val exerciseDataModel = ExerciseDataModel("Exercise 1") 33 | exerciseManager.update(exerciseDataModel).addOnCompleteListener { 34 | if (it.isSuccessful) { 35 | exerciseDataManager.showNew() 36 | exerciseDataManager.exerciseDataModel = exerciseDataModel 37 | } else { 38 | snackbar(view!!, it.exception?.localizedMessage ?: "") 39 | } 40 | } 41 | } else { 42 | exerciseManager.key = key 43 | exerciseManager.get().addListenerForSingleValueEvent(this) 44 | } 45 | } 46 | 47 | override fun onCancelled(p0: DatabaseError?) { 48 | snackbar(view!!, p0?.message ?: "") 49 | } 50 | 51 | override fun onDataChange(p0: DataSnapshot?) { 52 | exerciseDataManager.exerciseDataModel = p0!!.getValue(ExerciseDataModel::class.java)!! 53 | exerciseDataManager.showSeries() 54 | } 55 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/exercise/fragments/NewFragment.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.exercise.fragments 2 | 3 | import android.os.Bundle 4 | import android.text.format.DateUtils 5 | import android.view.View 6 | import android.widget.ArrayAdapter 7 | import com.morcinek.workout.R 8 | import com.morcinek.workout.common.fragment.BaseFragment 9 | import com.morcinek.workout.common.utils.formatWith 10 | import com.morcinek.workout.common.utils.timeFormat 11 | import com.morcinek.workout.core.data.exercises.ExerciseDataModel 12 | import com.morcinek.workout.exercise.ExerciseDataManager 13 | import com.morcinek.workout.exercise.exerciseComponent 14 | import kotlinx.android.synthetic.main.exercise_new.* 15 | import org.jetbrains.anko.sdk25.coroutines.onItemSelectedListener 16 | import org.jetbrains.anko.sdk25.coroutines.textChangedListener 17 | import java.util.* 18 | import javax.inject.Inject 19 | 20 | 21 | class NewFragment : BaseFragment() { 22 | 23 | override val layoutResourceId = R.layout.exercise_new 24 | 25 | @Inject lateinit var exerciseDataManager: ExerciseDataManager 26 | 27 | private val timeFormat by lazy { timeFormat() } 28 | 29 | override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { 30 | super.onViewCreated(view, savedInstanceState) 31 | exerciseComponent.inject(this) 32 | 33 | val exerciseDataModel = exerciseDataManager.exerciseDataModel 34 | setupName(exerciseDataModel) 35 | setupDate(exerciseDataModel) 36 | setupCategory(exerciseDataModel) 37 | 38 | breakButton.setOnClickListener { exerciseDataManager.showBreak() } 39 | exerciseButton.setOnClickListener { exerciseDataManager.showSeries() } 40 | } 41 | 42 | private fun setupName(exerciseDataModel: ExerciseDataModel) { 43 | nameEditText.setText(exerciseDataModel.name) 44 | nameEditText.textChangedListener { 45 | onTextChanged { charSequence, _, _, _ -> 46 | exerciseDataModel.name = charSequence.toString() 47 | } 48 | } 49 | } 50 | 51 | private fun setupDate(exerciseDataModel: ExerciseDataModel) { 52 | dateTextView.text = formatDate(exerciseDataModel.date) 53 | } 54 | 55 | private fun formatDate(date: Calendar) = "${relativeDateFormat(date)}, ${date.formatWith(timeFormat)}" 56 | 57 | private fun relativeDateFormat(date: Calendar) 58 | = DateUtils.getRelativeTimeSpanString(date.timeInMillis, Date().time, DateUtils.DAY_IN_MILLIS, DateUtils.FORMAT_SHOW_YEAR).toString() 59 | 60 | private fun setupCategory(exerciseDataModel: ExerciseDataModel) { 61 | val adapter = ArrayAdapter.createFromResource(context, R.array.categories, R.layout.spinner_layout) 62 | categorySpinner.adapter = adapter 63 | adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) 64 | categorySpinner.setSelection(adapter.getPosition(exerciseDataModel.category)) 65 | categorySpinner.onItemSelectedListener { 66 | onItemSelected { _, _, position, _ -> exerciseDataModel.category = adapter.getItem(position).toString() } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/exercise/fragments/SeriesFragment.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.exercise.fragments 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import com.morcinek.workout.R 6 | import com.morcinek.workout.common.fragment.BaseFragment 7 | import com.morcinek.workout.exercise.ExerciseDataManager 8 | import com.morcinek.workout.exercise.exerciseComponent 9 | import kotlinx.android.synthetic.main.exercise_series.* 10 | import javax.inject.Inject 11 | 12 | class SeriesFragment : BaseFragment() { 13 | 14 | override val layoutResourceId = R.layout.exercise_series 15 | 16 | @Inject lateinit var exerciseDataManager: ExerciseDataManager 17 | 18 | override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { 19 | super.onViewCreated(view, savedInstanceState) 20 | exerciseComponent.inject(this) 21 | 22 | seriesNumberText.text = "${exerciseDataManager.numberOfSeries}" 23 | breakButton.setOnClickListener { exerciseDataManager.showBreak() } 24 | finishButton.setOnClickListener { activity.onBackPressed() } 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/home/HomeActivity.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.home 2 | 3 | import android.os.Bundle 4 | import android.support.v7.app.AppCompatActivity 5 | import android.view.Menu 6 | import android.view.MenuItem 7 | import com.morcinek.workout.R 8 | import com.morcinek.workout.common.di.component 9 | import com.morcinek.workout.common.fragment.ContentFragmentManager 10 | import com.morcinek.workout.common.fragment.SingleFragmentActivity 11 | import com.morcinek.workout.common.utils.putSerializableExtra 12 | import com.morcinek.workout.common.utils.startActivityFun 13 | import com.morcinek.workout.exercise.ExerciseActivity 14 | import com.morcinek.workout.home.exercises.ExercisesFragment 15 | import com.morcinek.workout.settings.SettingsFragment 16 | import kotlinx.android.synthetic.main.home.* 17 | import org.jetbrains.anko.startActivity 18 | 19 | class HomeActivity : AppCompatActivity() { 20 | 21 | private val contentFragmentManager by lazy { ContentFragmentManager(this)} 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | super.onCreate(savedInstanceState) 25 | setContentView(R.layout.home) 26 | component.inject(this) 27 | 28 | setSupportActionBar(toolbar) 29 | 30 | fab.setOnClickListener { startActivity() } 31 | 32 | contentFragmentManager.replaceFragment(ExercisesFragment()) 33 | } 34 | 35 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 36 | menuInflater.inflate(R.menu.home, menu) 37 | return true 38 | } 39 | 40 | override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { 41 | R.id.action_settings -> { 42 | startActivityFun { putSerializableExtra(SettingsFragment::class.java) } 43 | true 44 | } 45 | else -> super.onOptionsItemSelected(item) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/home/exercises/ExercisesFragment.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.home.exercises 2 | 3 | import android.os.Bundle 4 | import android.support.v7.widget.LinearLayoutManager 5 | import android.view.View 6 | import com.morcinek.kotlin.adapter.SectionRecyclerViewAdapter 7 | import com.morcinek.workout.R 8 | import com.morcinek.workout.common.di.component 9 | import com.morcinek.workout.common.firebase.data.InteractorDelegate 10 | import com.morcinek.workout.common.fragment.BaseFragment 11 | import com.morcinek.workout.common.utils.startActivityFun 12 | import com.morcinek.workout.core.data.putKeyExtra 13 | import com.morcinek.workout.exercise.ExerciseActivity 14 | import com.morcinek.workout.home.exercises.adapter.ExerciseViewAdapter 15 | import com.morcinek.workout.home.exercises.adapter.ExerciseViewModel 16 | import kotlinx.android.synthetic.main.recycler_view.* 17 | import org.jetbrains.anko.design.snackbar 18 | import javax.inject.Inject 19 | 20 | class ExercisesFragment : BaseFragment(), InteractorDelegate> { 21 | 22 | override val layoutResourceId = R.layout.recycler_view 23 | 24 | @Inject lateinit var exercisesInteractor: ExercisesInteractor 25 | 26 | private val adapter: SectionRecyclerViewAdapter 27 | get() = recyclerView.adapter as SectionRecyclerViewAdapter 28 | 29 | override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { 30 | super.onViewCreated(view, savedInstanceState) 31 | component.inject(this) 32 | 33 | setupRecyclerView() 34 | exercisesInteractor.register(this) 35 | } 36 | 37 | override fun onDestroyView() { 38 | super.onDestroyView() 39 | exercisesInteractor.unregister() 40 | } 41 | 42 | override fun success(values: List) = adapter.setList(values) 43 | 44 | override fun failed(errorMessage: String) { 45 | snackbar(view!!, errorMessage) 46 | } 47 | 48 | private fun setupRecyclerView() { 49 | recyclerView.layoutManager = LinearLayoutManager(activity) 50 | recyclerView.adapter = SectionRecyclerViewAdapter().apply { 51 | addSectionViewAdapter(ExerciseViewAdapter()) 52 | setItemClickListener { 53 | activity.startActivityFun { 54 | it as ExerciseViewModel 55 | putKeyExtra(it.key) 56 | } 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/home/exercises/ExercisesInteractor.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.home.exercises 2 | 3 | import android.content.Context 4 | import android.text.format.DateUtils 5 | import com.morcinek.workout.R 6 | import com.morcinek.workout.common.firebase.data.DataProvider 7 | import com.morcinek.workout.common.firebase.data.InteractorDelegate 8 | import com.morcinek.workout.common.utils.dayOfWeekFormat 9 | import com.morcinek.workout.common.utils.formatWith 10 | import com.morcinek.workout.common.utils.timeFormat 11 | import com.morcinek.workout.core.data.exercises.ExerciseDataModel 12 | import com.morcinek.workout.core.data.exercises.ExercisesProvider 13 | import com.morcinek.workout.home.exercises.adapter.ExerciseViewModel 14 | import java.util.* 15 | 16 | class ExercisesInteractor(private val context: Context, private val exercisesProvider: ExercisesProvider) : DataProvider.Delegate { 17 | 18 | private val timeFormat by lazy { timeFormat() } 19 | private val dayOfWeekFormat by lazy { dayOfWeekFormat() } 20 | 21 | lateinit var delegate: InteractorDelegate> 22 | 23 | fun register(delegate: InteractorDelegate>) { 24 | this.delegate = delegate 25 | exercisesProvider.delegate = this 26 | exercisesProvider.register() 27 | } 28 | 29 | fun unregister() = exercisesProvider.unregister() 30 | 31 | override fun failed(errorMessage: String) = delegate.failed(errorMessage) 32 | 33 | override fun success(values: List>) = delegate.success(values.sortedByDescending { it.second.timeInMillis }.map { 34 | val exerciseDataModel = it.second 35 | ExerciseViewModel(it.first, exerciseDataModel.name, exerciseDataModel.category ?: "", dateFormat(exerciseDataModel.date), timeText(exerciseDataModel)) 36 | }) 37 | 38 | private fun timeText(exerciseDataModel: ExerciseDataModel) = context.getString(R.string.exercise_item_name, exerciseDataModel.numberOfSeries, timeFormat(exerciseDataModel.date)) 39 | 40 | private fun timeFormat(date: Calendar) = date.formatWith(timeFormat) 41 | 42 | private fun dateFormat(date: Calendar) = "${relativeDateFormat(date)}, ${dayOfWeekFormat(date)}" 43 | 44 | private fun dayOfWeekFormat(date: Calendar) = date.formatWith(dayOfWeekFormat) 45 | 46 | private fun relativeDateFormat(date: Calendar) 47 | = DateUtils.getRelativeTimeSpanString(date.timeInMillis, Date().time, DateUtils.DAY_IN_MILLIS, DateUtils.FORMAT_SHOW_YEAR).toString() 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/home/exercises/adapter/ExerciseViewAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.home.exercises.adapter 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.TextView 8 | import com.morcinek.kotlin.adapter.SectionRecyclerViewAdapter 9 | import com.morcinek.workout.R 10 | import kotlinx.android.synthetic.main.exercises_item.view.* 11 | 12 | /** 13 | * Copyright 2017 Tomasz Morcinek. All rights reserved. 14 | */ 15 | class ExerciseViewAdapter : SectionRecyclerViewAdapter.SectionViewAdapter { 16 | 17 | override fun onBindViewHolder(holder: ViewHolder, item: ExerciseViewModel, position: Int) { 18 | holder.name.text = item.name 19 | holder.category.text = item.category 20 | holder.date.text = item.date 21 | holder.time.text = item.time 22 | } 23 | 24 | override fun onCreateViewHolder(layoutInflater: LayoutInflater, parent: ViewGroup, viewType: Int) 25 | = ViewHolder(layoutInflater.inflate(R.layout.exercises_item, parent, false)) 26 | 27 | override fun clickableViews(holder: ViewHolder) = listOf(holder.itemView) 28 | 29 | inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 30 | val name: TextView 31 | get() = itemView.name 32 | val category: TextView 33 | get() = itemView.category 34 | val date: TextView 35 | get() = itemView.date 36 | val time: TextView 37 | get() = itemView.time 38 | } 39 | } 40 | 41 | class ExerciseViewModel(val key: String, val name: String, val category: String, val date: String, val time: String) -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/settings/SettingsFragment.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.settings 2 | 3 | import android.content.SharedPreferences 4 | import android.os.Bundle 5 | import android.support.v4.app.DialogFragment 6 | import android.support.v7.preference.Preference 7 | import android.support.v7.preference.PreferenceFragmentCompat 8 | import com.morcinek.workout.R 9 | import com.morcinek.workout.common.preference.* 10 | import com.morcinek.workout.common.utils.setTitle 11 | import org.jetbrains.anko.bundleOf 12 | 13 | 14 | class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener { 15 | 16 | override fun onActivityCreated(savedInstanceState: Bundle?) { 17 | super.onActivityCreated(savedInstanceState) 18 | setTitle(R.string.settings_label) 19 | } 20 | 21 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 22 | addPreferencesFromResource(R.xml.settings) 23 | 24 | updateGeneralBreakTime(preferenceManager.sharedPreferences) 25 | updateNotificationVibrationTime(preferenceManager.sharedPreferences) 26 | } 27 | 28 | override fun onDisplayPreferenceDialog(preference: Preference?) = 29 | if (preference is TimePreference) { 30 | showDialogFragment(TimePreferenceDialogFragmentCompat().apply { 31 | arguments = bundleOf( 32 | ARG_KEY to preference.getKey(), 33 | ARG_MIN to 10, 34 | ARG_MAX to 200, 35 | ARG_INCREMENT to 5 36 | ) 37 | }) 38 | } else { 39 | super.onDisplayPreferenceDialog(preference) 40 | } 41 | 42 | private fun showDialogFragment(dialogFragment: DialogFragment) { 43 | dialogFragment.setTargetFragment(this, 0) 44 | dialogFragment.show(this.fragmentManager, "android.support.v7.preference.PreferenceFragment.DIALOG") 45 | } 46 | 47 | override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { 48 | when (key) { 49 | VIBRATION_ENABLED -> updateNotificationVibrationTime(sharedPreferences) 50 | BREAK_TIME -> updateGeneralBreakTime(sharedPreferences) 51 | } 52 | } 53 | 54 | private fun updateGeneralBreakTime(sharedPreferences: SharedPreferences?) { 55 | preferenceScreen.findPreference(BREAK_TIME).summary = getString(R.string.breakTimeSummary, sharedPreferences?.breakTime) 56 | } 57 | 58 | private fun updateNotificationVibrationTime(sharedPreferences: SharedPreferences?) { 59 | preferenceScreen.findPreference(VIBRATION_VALUE).isEnabled = sharedPreferences!!.vibrationEnabled 60 | } 61 | 62 | override fun onResume() { 63 | super.onResume() 64 | preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this) 65 | } 66 | 67 | override fun onPause() { 68 | super.onPause() 69 | preferenceManager.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/settings/SettingsPreferencesUtils.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.settings 2 | 3 | import android.content.SharedPreferences 4 | import com.morcinek.workout.common.utils.getBoolean 5 | import com.morcinek.workout.common.utils.getInt 6 | 7 | 8 | const val BREAK_TIME = "generalBreakTime" 9 | const val VIBRATION_ENABLED = "notificationVibrationEnabled" 10 | const val VIBRATION_VALUE = "notificationVibrationValue" 11 | const val SOUND_ENABLED = "notificationSoundEnabled" 12 | 13 | inline val SharedPreferences.breakTime: Int 14 | get() = getInt(BREAK_TIME) 15 | // set(value) = applyFunction { putInt(BREAK_TIME, value) } 16 | 17 | inline val SharedPreferences.vibrationEnabled: Boolean 18 | get() = getBoolean(VIBRATION_ENABLED) 19 | // set(value) = applyFunction { putBoolean(VIBRATION_ENABLED, value) } 20 | 21 | inline val SharedPreferences.vibrationValue: Int 22 | get() = getInt(VIBRATION_VALUE) 23 | // set(value) = applyFunction { putInt(VIBRATION_VALUE, value) } 24 | 25 | inline val SharedPreferences.soundEnabled: Boolean 26 | get() = getBoolean(SOUND_ENABLED) 27 | // set(value) = applyFunction { putBoolean(SOUND_ENABLED, value) } 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/morcinek/workout/splash/SplashActivity.kt: -------------------------------------------------------------------------------- 1 | package com.morcinek.workout.splash 2 | 3 | import android.os.Bundle 4 | import android.support.v7.app.AppCompatActivity 5 | import com.morcinek.workout.common.di.component 6 | import com.morcinek.workout.common.firebase.auth.UserManager 7 | import com.morcinek.workout.home.HomeActivity 8 | import org.jetbrains.anko.startActivity 9 | import javax.inject.Inject 10 | 11 | class SplashActivity : AppCompatActivity() { 12 | 13 | @Inject lateinit var userManager: UserManager 14 | 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | component.inject(this) 18 | 19 | if (!userManager.isUserAuthenticated) { 20 | authenticateUser() 21 | } else { 22 | startNextActivity() 23 | } 24 | } 25 | 26 | private fun startNextActivity() { 27 | startActivity() 28 | finish() 29 | } 30 | 31 | private fun authenticateUser() { 32 | userManager.signInAnonymously().addOnCompleteListener { 33 | if (it.isSuccessful) { 34 | startNextActivity() 35 | } else { 36 | finish() 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/oval_accent.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/oval_primary.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmorcinek/kotlin-android-tutorial/12c003eaecc1a9b4f0330b8c91d9a8c8ec70e86c/app/src/main/res/drawable-xhdpi/ic_delete.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmorcinek/kotlin-android-tutorial/12c003eaecc1a9b4f0330b8c91d9a8c8ec70e86c/app/src/main/res/drawable-xhdpi/ic_edit.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmorcinek/kotlin-android-tutorial/12c003eaecc1a9b4f0330b8c91d9a8c8ec70e86c/app/src/main/res/drawable-xhdpi/ic_settings.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmorcinek/kotlin-android-tutorial/12c003eaecc1a9b4f0330b8c91d9a8c8ec70e86c/app/src/main/res/drawable-xhdpi/logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmorcinek/kotlin-android-tutorial/12c003eaecc1a9b4f0330b8c91d9a8c8ec70e86c/app/src/main/res/drawable-xxhdpi/ic_delete.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmorcinek/kotlin-android-tutorial/12c003eaecc1a9b4f0330b8c91d9a8c8ec70e86c/app/src/main/res/drawable-xxhdpi/ic_edit.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmorcinek/kotlin-android-tutorial/12c003eaecc1a9b4f0330b8c91d9a8c8ec70e86c/app/src/main/res/drawable-xxhdpi/ic_settings.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmorcinek/kotlin-android-tutorial/12c003eaecc1a9b4f0330b8c91d9a8c8ec70e86c/app/src/main/res/drawable-xxhdpi/logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmorcinek/kotlin-android-tutorial/12c003eaecc1a9b4f0330b8c91d9a8c8ec70e86c/app/src/main/res/drawable-xxxhdpi/ic_delete.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmorcinek/kotlin-android-tutorial/12c003eaecc1a9b4f0330b8c91d9a8c8ec70e86c/app/src/main/res/drawable-xxxhdpi/ic_edit.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmorcinek/kotlin-android-tutorial/12c003eaecc1a9b4f0330b8c91d9a8c8ec70e86c/app/src/main/res/drawable-xxxhdpi/ic_settings.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmorcinek/kotlin-android-tutorial/12c003eaecc1a9b4f0330b8c91d9a8c8ec70e86c/app/src/main/res/drawable-xxxhdpi/logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/oval_accent.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/oval_primary.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/progress_bar_accent.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 10 | 11 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/progress_bar_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/splash_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 20 | 21 | 22 | 23 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/exercise_break.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 |