commit 607996f26025c159c6ee36d03731a8370a5d54d1 Author: William Brawner Date: Fri May 29 14:48:39 2020 -0700 Initial commit Signed-off-by: William Brawner diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..603b140 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..b9531a9 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Interval Timer \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..adef587 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,123 @@ + + + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+ + +
+
\ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..d7ed158 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/dictionaries/billybrawner.xml b/.idea/dictionaries/billybrawner.xml new file mode 100644 index 0000000..3a02d6a --- /dev/null +++ b/.idea/dictionaries/billybrawner.xml @@ -0,0 +1,7 @@ + + + + trainterval + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..5cd135a --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..a5f05cd --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..7bfef59 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..118b7af --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,89 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.3" + + defaultConfig { + applicationId "com.wbrawner.trainterval" + minSdkVersion 23 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + javaCompileOptions { + annotationProcessorOptions { + arguments = [ + "room.schemaLocation" : "$projectDir/schemas".toString(), + "room.incremental" : "true", + "room.expandProjection": "true"] + } + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.3.5' + + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.core:core-ktx:1.2.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + + implementation 'com.google.android.material:material:1.2.0-alpha05' + implementation 'androidx.recyclerview:recyclerview:1.1.0' + + def room_version = "2.2.5" + implementation "androidx.room:room-runtime:$room_version" + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + kapt "androidx.room:room-compiler:$room_version" + implementation "androidx.room:room-ktx:$room_version" + testImplementation "androidx.room:room-testing:$room_version" + + def nav_version = "2.2.1" + implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" + implementation "androidx.navigation:navigation-ui-ktx:$nav_version" + // Requires 2.3.0 which is still in alpha at the time of writing + // androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" + + def lifecycle_version = "2.2.0" + def arch_version = "2.1.0" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" + testImplementation "androidx.arch.core:core-testing:$arch_version" + + def koin_version = "2.1.5" + implementation "org.koin:koin-core:$koin_version" + implementation "org.koin:koin-android:$koin_version" + testImplementation "org.koin:koin-test:$koin_version" + + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# 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 *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/schemas/com.wbrawner.trainterval.TraintervalDatabase/1.json b/app/schemas/com.wbrawner.trainterval.TraintervalDatabase/1.json new file mode 100644 index 0000000..e15570b --- /dev/null +++ b/app/schemas/com.wbrawner.trainterval.TraintervalDatabase/1.json @@ -0,0 +1,88 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "03572fc3000245d00e3b4f85a3162fdd", + "entities": [ + { + "tableName": "interval_timer", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `name` TEXT NOT NULL, `description` TEXT NOT NULL, `warmUpDuration` INTEGER NOT NULL, `lowIntensityDuration` INTEGER NOT NULL, `highIntensityDuration` INTEGER NOT NULL, `restDuration` INTEGER NOT NULL, `coolDownDuration` INTEGER NOT NULL, `sets` INTEGER NOT NULL, `cycles` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "warmUpDuration", + "columnName": "warmUpDuration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lowIntensityDuration", + "columnName": "lowIntensityDuration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "highIntensityDuration", + "columnName": "highIntensityDuration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "restDuration", + "columnName": "restDuration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "coolDownDuration", + "columnName": "coolDownDuration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sets", + "columnName": "sets", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cycles", + "columnName": "cycles", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '03572fc3000245d00e3b4f85a3162fdd')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/wbrawner/trainterval/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/wbrawner/trainterval/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..531952e --- /dev/null +++ b/app/src/androidTest/java/com/wbrawner/trainterval/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.wbrawner.trainterval + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.wbrawner.intervaltimer", appContext.packageName) + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..847f9f4 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/wbrawner/trainterval/DurationPicker.kt b/app/src/main/java/com/wbrawner/trainterval/DurationPicker.kt new file mode 100644 index 0000000..87322e3 --- /dev/null +++ b/app/src/main/java/com/wbrawner/trainterval/DurationPicker.kt @@ -0,0 +1,66 @@ +package com.wbrawner.trainterval + +import android.content.Context +import android.util.AttributeSet +import android.util.Log +import android.view.LayoutInflater +import android.widget.FrameLayout +import android.widget.NumberPicker +import androidx.annotation.AttrRes +import androidx.annotation.StyleRes +import androidx.core.math.MathUtils +import java.util.concurrent.TimeUnit + +private const val MIN_DURATION = 0L +private val MAX_DURATION = TimeUnit.DAYS.toMillis(1) + +class DurationPicker @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + @AttrRes defStyleAttr: Int = 0, + @StyleRes defStyleRes: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) { + + private var intervalDuration = IntervalDuration() + var duration: Long + get() = intervalDuration.toMillis() + set(value) { + intervalDuration = MathUtils.clamp(value, MIN_DURATION, MAX_DURATION).toIntervalDuration() + hours.value = intervalDuration.hours.toInt() + minutes.value = intervalDuration.minutes.toInt() + seconds.value = intervalDuration.seconds.toInt() + } + private val hours: NumberPicker + private val minutes: NumberPicker + private val seconds: NumberPicker + + init { + val view = LayoutInflater.from(context).inflate(R.layout.duration_picker, this, true) + hours = view.findViewById(R.id.hours) + hours.minValue = 0 + hours.maxValue = 23 + hours.setOnValueChangedListener { _, _, newVal -> + intervalDuration = intervalDuration.copy(hours = newVal.toLong()) + Log.v("DurationPicker", "Updated hours: $newVal") + } + hours.setFormatter(numberFormatter()) + minutes = view.findViewById(R.id.minutes) + minutes.minValue = 0 + minutes.maxValue = 59 + minutes.setOnValueChangedListener { _, _, newVal -> + intervalDuration = intervalDuration.copy(minutes = newVal.toLong()) + Log.v("DurationPicker", "Updated minutes: $newVal") + } + minutes.setFormatter(numberFormatter()) + seconds = view.findViewById(R.id.seconds) + seconds.minValue = 0 + seconds.maxValue = 59 + seconds.setOnValueChangedListener { _, _, newVal -> + intervalDuration = intervalDuration.copy(seconds = newVal.toLong()) + Log.v("DurationPicker", "Updated seconds: $newVal") + } + seconds.setFormatter(numberFormatter()) + } + + private fun numberFormatter() = NumberPicker.Formatter { "%02d".format(it) } +} diff --git a/app/src/main/java/com/wbrawner/trainterval/IntervalDuration.kt b/app/src/main/java/com/wbrawner/trainterval/IntervalDuration.kt new file mode 100644 index 0000000..ea7b4ee --- /dev/null +++ b/app/src/main/java/com/wbrawner/trainterval/IntervalDuration.kt @@ -0,0 +1,44 @@ +package com.wbrawner.trainterval + +import java.util.concurrent.TimeUnit + + +data class IntervalDuration( + val hours: Long = 0, + val minutes: Long = 0, + val seconds: Long = 0 +) { + override fun toString(): String { + return "%02d:%02d:%02d".format(hours, minutes, seconds) + } +} + +fun IntervalDuration.toMillis(): Long { + return TimeUnit.HOURS.toMillis(hours) + + TimeUnit.MINUTES.toMillis(minutes) + + TimeUnit.SECONDS.toMillis(seconds) +} + +fun Long.toIntervalDuration(): IntervalDuration { + val SECONDS_IN_HOUR = 3600 + val SECONDS_IN_MINUTE = 60 + + if (this < 1000) { + return IntervalDuration(0, 0, 0) + } + + var seconds: Long = this / 1000 + var hours: Long = 0 + if (seconds >= SECONDS_IN_HOUR) { + hours = seconds / SECONDS_IN_HOUR + seconds -= hours * SECONDS_IN_HOUR + } + + var minutes: Long = 0 + if (seconds >= SECONDS_IN_MINUTE) { + minutes = seconds / SECONDS_IN_MINUTE + seconds -= minutes * SECONDS_IN_MINUTE + } + + return IntervalDuration(hours, minutes, seconds) +} \ No newline at end of file diff --git a/app/src/main/java/com/wbrawner/trainterval/Logger.kt b/app/src/main/java/com/wbrawner/trainterval/Logger.kt new file mode 100644 index 0000000..75ad3f4 --- /dev/null +++ b/app/src/main/java/com/wbrawner/trainterval/Logger.kt @@ -0,0 +1,34 @@ +package com.wbrawner.trainterval + +import android.util.Log + +interface Logger { + val defaultTag: String + fun v(tag: String = defaultTag, message: String) + fun d(tag: String = defaultTag, message: String) + fun i(tag: String = defaultTag, message: String) + fun w(tag: String = defaultTag, message: String) + fun e(tag: String = defaultTag, message: String, error: Throwable? = null) +} + +class AndroidLogger(override val defaultTag: String) : Logger { + override fun v(tag: String, message: String) { + Log.v(tag, message) + } + + override fun d(tag: String, message: String) { + Log.d(tag, message) + } + + override fun i(tag: String, message: String) { + Log.i(tag, message) + } + + override fun w(tag: String, message: String) { + Log.w(tag, message) + } + + override fun e(tag: String, message: String, error: Throwable?) { + Log.e(tag, message, error) + } +} diff --git a/app/src/main/java/com/wbrawner/trainterval/MainActivity.kt b/app/src/main/java/com/wbrawner/trainterval/MainActivity.kt new file mode 100644 index 0000000..ff14239 --- /dev/null +++ b/app/src/main/java/com/wbrawner/trainterval/MainActivity.kt @@ -0,0 +1,12 @@ +package com.wbrawner.trainterval + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + } +} diff --git a/app/src/main/java/com/wbrawner/trainterval/TraintervalActivityLifecycleCallbacks.kt b/app/src/main/java/com/wbrawner/trainterval/TraintervalActivityLifecycleCallbacks.kt new file mode 100644 index 0000000..4a070f5 --- /dev/null +++ b/app/src/main/java/com/wbrawner/trainterval/TraintervalActivityLifecycleCallbacks.kt @@ -0,0 +1,43 @@ +package com.wbrawner.trainterval + +import android.app.Activity +import android.app.Application +import android.os.Bundle + +private const val TAG = "ActivityLifecycleCallbacks" + +class TraintervalActivityLifecycleCallbacks( + private val logger: Logger +) : Application.ActivityLifecycleCallbacks { + private var currentActivity: Activity? = null + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + logger.v(TAG, "onActivityCreated: $activity") + } + + override fun onActivityStarted(activity: Activity) { + logger.v(TAG, "onActivityStarted: $activity") + currentActivity = activity + } + + override fun onActivityResumed(activity: Activity) { + logger.v(TAG, "onActivityResumed: $activity") + } + + override fun onActivityPaused(activity: Activity) { + logger.v(TAG, "onActivityPaused: $activity") + } + + override fun onActivityStopped(activity: Activity) { + logger.v(TAG, "onActivityStopped: $activity") + currentActivity = null + } + + override fun onActivityDestroyed(activity: Activity) { + logger.v(TAG, "onActivityDestroyed: $activity") + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { + logger.v(TAG, "onActivitySaveInstanceState: $activity") + } +} diff --git a/app/src/main/java/com/wbrawner/trainterval/TraintervalApplication.kt b/app/src/main/java/com/wbrawner/trainterval/TraintervalApplication.kt new file mode 100644 index 0000000..e0e7989 --- /dev/null +++ b/app/src/main/java/com/wbrawner/trainterval/TraintervalApplication.kt @@ -0,0 +1,56 @@ +package com.wbrawner.trainterval + +import android.app.Application +import androidx.room.Room +import com.wbrawner.trainterval.activetimer.ActiveTimerViewModel +import com.wbrawner.trainterval.timerform.TimerFormViewModel +import com.wbrawner.trainterval.timerlist.TimerListViewModel +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.context.startKoin +import org.koin.core.parameter.parametersOf +import org.koin.dsl.module + +class TraintervalApplication : Application() { + + override fun onCreate() { + super.onCreate() + + startKoin { + androidLogger() + androidContext(this@TraintervalApplication) + modules(traintervalModule) + } + + val lifecycleCallbacks = + TraintervalActivityLifecycleCallbacks(AndroidLogger("LifecycleCallbacks")) + registerActivityLifecycleCallbacks(lifecycleCallbacks) + } +} + +val traintervalModule = module { + single { + Room.databaseBuilder(get(), TraintervalDatabase::class.java, "trainterval") + .build() + } + + single { + get().timerDao() + } + + single { + TimerListViewModel(get(parameters = { parametersOf("TimerListStore") }), get()) + } + + factory { + TimerFormViewModel(get(parameters = { parametersOf("TimerFormStore") }), get()) + } + + factory { + ActiveTimerViewModel(get(parameters = { parametersOf("ActiveTimerStore") }), get()) + } + + factory { params -> + AndroidLogger(params.component1()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wbrawner/trainterval/TraintervalDatabase.kt b/app/src/main/java/com/wbrawner/trainterval/TraintervalDatabase.kt new file mode 100644 index 0000000..81b1c50 --- /dev/null +++ b/app/src/main/java/com/wbrawner/trainterval/TraintervalDatabase.kt @@ -0,0 +1,11 @@ +package com.wbrawner.trainterval + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.wbrawner.trainterval.model.IntervalTimer +import com.wbrawner.trainterval.model.IntervalTimerDao + +@Database(entities = [IntervalTimer::class], version = 1) +abstract class TraintervalDatabase : RoomDatabase() { + abstract fun timerDao(): IntervalTimerDao +} \ No newline at end of file diff --git a/app/src/main/java/com/wbrawner/trainterval/activetimer/ActiveTimerFragment.kt b/app/src/main/java/com/wbrawner/trainterval/activetimer/ActiveTimerFragment.kt new file mode 100644 index 0000000..846c807 --- /dev/null +++ b/app/src/main/java/com/wbrawner/trainterval/activetimer/ActiveTimerFragment.kt @@ -0,0 +1,77 @@ +package com.wbrawner.trainterval.activetimer + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import com.wbrawner.trainterval.R +import kotlinx.android.synthetic.main.fragment_active_timer.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import org.koin.android.ext.android.inject + +class ActiveTimerFragment : Fragment() { + + private var coroutineScope: CoroutineScope? = null + private val activeTimerViewModel: ActiveTimerViewModel by inject() + private var timerId: Long = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + timerId = requireArguments().getLong("timerId") + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_active_timer, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + coroutineScope = CoroutineScope(Dispatchers.Main) + coroutineScope!!.launch { + activeTimerViewModel.timerState.observe(viewLifecycleOwner, Observer { state -> + when (state) { + is IntervalTimerActiveState.LoadingState -> renderLoading() + is IntervalTimerActiveState.TimerRunningState -> renderTimer(state) + is IntervalTimerActiveState.ExitState -> findNavController().navigateUp() + } + }) + } + coroutineScope!!.launch { + activeTimerViewModel.init(timerId) + } + playPauseButton.setOnClickListener { + coroutineScope!!.launch { + activeTimerViewModel.toggleTimer() + } + } + } + + private fun renderLoading() { + progressBar.visibility = View.VISIBLE + timerLayout.visibility = View.GONE + } + + private fun renderTimer(state: IntervalTimerActiveState.TimerRunningState) { + progressBar.visibility = View.GONE + timerLayout.visibility = View.VISIBLE + playPauseButton.setImageDrawable(requireContext().getDrawable(state.playPauseIcon)) + timeRemaining.text = state.timeRemaining + } + + override fun onDestroyView() { + coroutineScope?.cancel() + coroutineScope = null + super.onDestroyView() + } + + companion object { + private const val EXTRA_TIMER_ID = "timerId" + } +} diff --git a/app/src/main/java/com/wbrawner/trainterval/activetimer/ActiveTimerViewModel.kt b/app/src/main/java/com/wbrawner/trainterval/activetimer/ActiveTimerViewModel.kt new file mode 100644 index 0000000..fde9335 --- /dev/null +++ b/app/src/main/java/com/wbrawner/trainterval/activetimer/ActiveTimerViewModel.kt @@ -0,0 +1,216 @@ +package com.wbrawner.trainterval.activetimer + +import androidx.annotation.DrawableRes +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.wbrawner.trainterval.Logger +import com.wbrawner.trainterval.R +import com.wbrawner.trainterval.activetimer.IntervalTimerActiveState.LoadingState +import com.wbrawner.trainterval.activetimer.IntervalTimerActiveState.TimerRunningState +import com.wbrawner.trainterval.model.IntervalTimer +import com.wbrawner.trainterval.model.IntervalTimerDao +import com.wbrawner.trainterval.model.Phase +import com.wbrawner.trainterval.toIntervalDuration +import kotlinx.coroutines.* +import kotlin.coroutines.coroutineContext + +class ActiveTimerViewModel( + private val logger: Logger, + private val timerDao: IntervalTimerDao +) : ViewModel() { + val timerState: MutableLiveData = MutableLiveData(LoadingState) + private var timerJob: Job? = null + private lateinit var timer: IntervalTimer + private var timerComplete = false + private var timerRunning = false + private var currentPhase = Phase.WARM_UP + private var currentSet = 1 + private var currentRound = 1 + private var timeRemaining: Long = 0 + + suspend fun init(timerId: Long) { + logger.d(message = "Initializing with Timer id $timerId") + timer = timerDao.getById(timerId) + timeRemaining = timer.warmUpDuration + timerState.postValue( + TimerRunningState( + timerRunning, + timeRemaining.toIntervalDuration().toString(), + currentSet, + timer.sets, + currentRound, + timer.cycles + ) + ) + } + + suspend fun toggleTimer() { + if (timerRunning) { + timerJob?.cancel() + timerRunning = false + timerState.postValue( + TimerRunningState( + timerRunning, + timeRemaining.toIntervalDuration().toString(), + currentSet, + timer.sets, + currentRound, + timer.cycles + ) + ) + } else { + coroutineScope { + timerJob = launch { + startTimer() + } + } + } + } + + private suspend fun startTimer() { + timerRunning = true + timerState.postValue( + TimerRunningState( + timerRunning, + timeRemaining.toIntervalDuration().toString(), + currentSet, + timer.sets, + currentRound, + timer.cycles + ) + ) + while (coroutineContext.isActive && timerRunning) { + delay(1_000) + timeRemaining -= 1_000 + if (timeRemaining <= 0) { + goForward() + } + timerState.postValue( + TimerRunningState( + timerRunning, + timeRemaining.toIntervalDuration().toString(), + currentSet, + timer.sets, + currentRound, + timer.cycles + ) + ) + } + } + + suspend fun skipAhead() { + timerJob?.cancel() + when (currentPhase) { + Phase.COOL_DOWN -> { + timeRemaining = 0 + } + else -> { + goForward() + } + } + if (timerRunning) { + startTimer() + } + } + + private fun goForward() { + timerComplete = currentPhase == Phase.COOL_DOWN + when (currentPhase) { + Phase.WARM_UP -> { + currentPhase = Phase.LOW_INTENSITY + timeRemaining = timer.lowIntensityDuration + } + Phase.LOW_INTENSITY -> { + currentPhase = Phase.HIGH_INTENSITY + timeRemaining = timer.highIntensityDuration + } + Phase.HIGH_INTENSITY -> { + when { + currentSet < timer.sets -> { + currentSet++ + currentPhase = Phase.LOW_INTENSITY + timeRemaining = timer.lowIntensityDuration + } + currentRound < timer.cycles -> { + currentRound++ + currentPhase = Phase.REST + timeRemaining = timer.restDuration + } + else -> { + currentPhase = Phase.COOL_DOWN + timeRemaining = timer.coolDownDuration + } + } + } + Phase.REST -> { + currentSet = 1 + currentPhase = Phase.LOW_INTENSITY + timeRemaining = timer.lowIntensityDuration + } + Phase.COOL_DOWN -> { + timerRunning = false + } + } + } + + suspend fun goBack() { + timerJob?.cancel() + when (currentPhase) { + Phase.WARM_UP -> { + timeRemaining = timer.warmUpDuration + } + Phase.LOW_INTENSITY -> { + when { + currentSet == 1 && currentRound == 1 -> { + currentPhase = Phase.WARM_UP + timeRemaining = timer.warmUpDuration + } + currentSet == 1 && currentRound > 1 -> { + currentPhase = Phase.REST + timeRemaining = timer.restDuration + } + else -> { + currentPhase = Phase.HIGH_INTENSITY + timeRemaining = timer.highIntensityDuration + } + } + timeRemaining = timer.highIntensityDuration + } + Phase.HIGH_INTENSITY -> { + currentPhase = Phase.LOW_INTENSITY + timeRemaining = timer.lowIntensityDuration + } + Phase.REST -> { + currentRound-- + currentPhase = Phase.HIGH_INTENSITY + currentSet = timer.sets + timeRemaining = timer.highIntensityDuration + } + Phase.COOL_DOWN -> { + currentPhase = Phase.HIGH_INTENSITY + timeRemaining = timer.highIntensityDuration + } + } + if (timerRunning) { + startTimer() + } + } +} + +/** + * Used to represent the state while a user has a specific timer open. + */ +sealed class IntervalTimerActiveState { + object LoadingState : IntervalTimerActiveState() + class TimerRunningState( + timerRunning: Boolean, + val timeRemaining: String, + val currentSet: Int, + val totalSets: Int, + val currentRound: Int, + val totalRounds: Int, + val timerComplete: Boolean = false, + @DrawableRes val playPauseIcon: Int = if (timerRunning) R.drawable.ic_pause else R.drawable.ic_play_arrow + ) : IntervalTimerActiveState() + object ExitState : IntervalTimerActiveState() +} diff --git a/app/src/main/java/com/wbrawner/trainterval/model/IntervalTimer.kt b/app/src/main/java/com/wbrawner/trainterval/model/IntervalTimer.kt new file mode 100644 index 0000000..c81edda --- /dev/null +++ b/app/src/main/java/com/wbrawner/trainterval/model/IntervalTimer.kt @@ -0,0 +1,45 @@ +package com.wbrawner.trainterval.model + +import androidx.room.* +import kotlinx.coroutines.flow.Flow +import java.util.concurrent.TimeUnit + +@Entity(tableName = "interval_timer") +data class IntervalTimer( + @PrimaryKey val id: Long? = null, + val name: String = "", + val description: String = "", + val warmUpDuration: Long = TimeUnit.MINUTES.toMillis(5), + val lowIntensityDuration: Long = TimeUnit.SECONDS.toMillis(30), + val highIntensityDuration: Long = TimeUnit.MINUTES.toMillis(1), + val restDuration: Long = TimeUnit.MINUTES.toMillis(1), + val coolDownDuration: Long = TimeUnit.MINUTES.toMillis(5), + val sets: Int = 4, + val cycles: Int = 1 +) + +enum class Phase { + WARM_UP, + LOW_INTENSITY, + HIGH_INTENSITY, + REST, + COOL_DOWN, +} + +@Dao +interface IntervalTimerDao { + @Query("SELECT * FROM interval_timer") + fun getAll(): Flow> + + @Query("SELECT * FROM interval_timer WHERE id = :id") + suspend fun getById(id: Long): IntervalTimer + + @Insert + suspend fun insert(timer: IntervalTimer): Long + + @Update + suspend fun update(timer: IntervalTimer) + + @Delete + suspend fun delete(timer: IntervalTimer) +} diff --git a/app/src/main/java/com/wbrawner/trainterval/timerform/TimerFormFragment.kt b/app/src/main/java/com/wbrawner/trainterval/timerform/TimerFormFragment.kt new file mode 100644 index 0000000..5c37152 --- /dev/null +++ b/app/src/main/java/com/wbrawner/trainterval/timerform/TimerFormFragment.kt @@ -0,0 +1,110 @@ +package com.wbrawner.trainterval.timerform + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import com.wbrawner.trainterval.R +import com.wbrawner.trainterval.model.IntervalTimer +import com.wbrawner.trainterval.timerform.IntervalTimerEditState.* +import kotlinx.android.synthetic.main.fragment_timer_form.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import org.koin.android.ext.android.inject + +class TimerFormFragment : Fragment() { + + private var coroutineScope: CoroutineScope? = null + private val timerFormViewModel: TimerFormViewModel by inject() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_timer_form, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + (activity as? AppCompatActivity)?.let { + it.setSupportActionBar(toolbar) + it.supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + coroutineScope = CoroutineScope(Dispatchers.Main) + coroutineScope!!.launch { + timerFormViewModel.timerState.observe(viewLifecycleOwner, Observer { state -> + when (state) { + is LoadingState -> renderLoading() + is EditTimerState -> renderEditingState(state.timer, state.title) + is EditTimerSavedState -> findNavController().navigate(R.id.timerListFragment) + is ErrorState -> renderErrorState(state.message) + } + }) + } + coroutineScope!!.launch { + timerFormViewModel.init(arguments?.getLong(EXTRA_TIMER_ID)) + } + timerSets.minValue = 1 + timerSets.maxValue = 99 + timerRepeat.minValue = 1 + timerRepeat.maxValue = 99 + saveButton.setOnClickListener { + coroutineScope?.launch { + timerFormViewModel.saveTimer( + timerName.text.toString(), + timerDescription.text.toString(), + warmUpDuration.duration, + lowIntensityDuration.duration, + highIntensityDuration.duration, + restDuration.duration, + coolDownDuration.duration, + timerSets.value, + timerRepeat.value + ) + } + } + } + + private fun renderLoading() { + timerForm.visibility = View.GONE + error.visibility = View.GONE + progressBar.visibility = View.VISIBLE + } + + private fun renderEditingState(timer: IntervalTimer, title: String) { + toolbar.title = title + timerForm.visibility = View.VISIBLE + timerName.setText(timer.name) + timerDescription.setText(timer.description) + warmUpDuration.duration = timer.warmUpDuration + lowIntensityDuration.duration = timer.lowIntensityDuration + highIntensityDuration.duration = timer.highIntensityDuration + restDuration.duration = timer.restDuration + coolDownDuration.duration = timer.coolDownDuration + timerSets.value = timer.sets + timerRepeat.value = timer.cycles + error.visibility = View.GONE + progressBar.visibility = View.GONE + } + + private fun renderErrorState(message: String) { + timerForm.visibility = View.GONE + error.visibility = View.VISIBLE + error.text = message + progressBar.visibility = View.GONE + } + + override fun onDestroyView() { + coroutineScope?.cancel() + coroutineScope = null + super.onDestroyView() + } + + companion object { + const val EXTRA_TIMER_ID = "timerId" + } +} diff --git a/app/src/main/java/com/wbrawner/trainterval/timerform/TimerFormViewModel.kt b/app/src/main/java/com/wbrawner/trainterval/timerform/TimerFormViewModel.kt new file mode 100644 index 0000000..18e0433 --- /dev/null +++ b/app/src/main/java/com/wbrawner/trainterval/timerform/TimerFormViewModel.kt @@ -0,0 +1,73 @@ +package com.wbrawner.trainterval.timerform + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.wbrawner.trainterval.Logger +import com.wbrawner.trainterval.model.IntervalTimer +import com.wbrawner.trainterval.model.IntervalTimerDao +import com.wbrawner.trainterval.timerform.IntervalTimerEditState.* + +class TimerFormViewModel( + private val logger: Logger, + private val timerDao: IntervalTimerDao +) : ViewModel() { + private lateinit var timer: IntervalTimer + val timerState: MutableLiveData = MutableLiveData(LoadingState) + + suspend fun init(timerId: Long? = null) { + timer = if (timerId == null) { + IntervalTimer() + } else { + timerDao.getById(timerId) + } + logger.v(message = "Timer: ") + logger.v(message = timer.toString()) + timerState.postValue(EditTimerState(timer)) + } + + suspend fun saveTimer( + name: String, + description: String, + warmUpDuration: Long, + lowIntensityDuration: Long, + highIntensityDuration: Long, + restDuration: Long, + coolDownDuration: Long, + sets: Int, + cycles: Int + ) { + timerState.postValue(LoadingState) + timer = timer.copy( + name = name, + description = description, + warmUpDuration = warmUpDuration, + lowIntensityDuration = lowIntensityDuration, + highIntensityDuration = highIntensityDuration, + restDuration = restDuration, + coolDownDuration = coolDownDuration, + sets = sets, + cycles = cycles + ) + if (timer.id != null) { + timerDao.update(timer) + } else { + timer = timer.copy(id = timerDao.insert(timer)) + } + timerState.postValue(EditTimerSavedState(timer)) + } +} + +/** + * Used to represent each state while a user is creating or editing a timer. + */ +sealed class IntervalTimerEditState { + object LoadingState : IntervalTimerEditState() + class EditTimerState( + val timer: IntervalTimer, + val title: String = if (timer.id != null) "Edit Timer" else "Add Timer", + val showDeleteButton: Boolean = timer.id != null + ) : IntervalTimerEditState() + + class EditTimerSavedState(val timer: IntervalTimer) : IntervalTimerEditState() + class ErrorState(val message: String) : IntervalTimerEditState() +} diff --git a/app/src/main/java/com/wbrawner/trainterval/timerlist/TimerListAdapter.kt b/app/src/main/java/com/wbrawner/trainterval/timerlist/TimerListAdapter.kt new file mode 100644 index 0000000..df42326 --- /dev/null +++ b/app/src/main/java/com/wbrawner/trainterval/timerlist/TimerListAdapter.kt @@ -0,0 +1,49 @@ +package com.wbrawner.trainterval.timerlist + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.wbrawner.trainterval.model.IntervalTimer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +class TimerListAdapter( + private val timerListViewModel: TimerListViewModel, + private val coroutineScope: CoroutineScope +) : ListAdapter(IntervalTimerDiffUtilCallback()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + LayoutInflater.from(parent.context) + .inflate(android.R.layout.simple_list_item_2, parent, false) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val timer = getItem(position) + holder.title.text = timer.name + holder.description.text = timer.description + holder.itemView.setOnClickListener { + coroutineScope.launch { + timerListViewModel.openTimer(timer) + } + } + } + + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val title: TextView = itemView.findViewById(android.R.id.text1) + val description: TextView = itemView.findViewById(android.R.id.text2) + } +} + +class IntervalTimerDiffUtilCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: IntervalTimer, newItem: IntervalTimer): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: IntervalTimer, newItem: IntervalTimer): Boolean { + return oldItem == newItem + } +} diff --git a/app/src/main/java/com/wbrawner/trainterval/timerlist/TimerListFragment.kt b/app/src/main/java/com/wbrawner/trainterval/timerlist/TimerListFragment.kt new file mode 100644 index 0000000..ddc991f --- /dev/null +++ b/app/src/main/java/com/wbrawner/trainterval/timerlist/TimerListFragment.kt @@ -0,0 +1,115 @@ +package com.wbrawner.trainterval.timerlist + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.wbrawner.trainterval.R +import com.wbrawner.trainterval.model.IntervalTimer +import com.wbrawner.trainterval.timerlist.IntervalTimerListState.* +import kotlinx.android.synthetic.main.fragment_timer_list.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import org.koin.android.ext.android.inject + +class TimerListFragment : Fragment() { + + private var coroutineScope: CoroutineScope? = null + private val timerListViewModel: TimerListViewModel by inject() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_timer_list, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + coroutineScope = CoroutineScope(Dispatchers.Main) + (activity as? AppCompatActivity)?.let { + it.setSupportActionBar(toolbar) + it.supportActionBar?.setDisplayHomeAsUpEnabled(false) + } + timerList.layoutManager = LinearLayoutManager(view.context) + timerList.addItemDecoration( + DividerItemDecoration( + view.context, + LinearLayoutManager.VERTICAL + ) + ) + timerList.adapter = TimerListAdapter(timerListViewModel, coroutineScope!!) + addTimerButton.setOnClickListener { + coroutineScope?.launch { + timerListViewModel.addTimer() + } + } + coroutineScope!!.launch { + timerListViewModel.timerState.observe(viewLifecycleOwner, Observer { state -> + when (state) { + is LoadingState -> renderLoading() + is EmptyListState -> renderEmptyList() + is SuccessListState -> renderSuccessState(state.timers) + is ErrorState -> renderErrorState(state.message) + is CreateTimer -> findNavController().navigate(R.id.timerFormFragment) + is EditTimer -> findNavController().navigate(R.id.timerFormFragment) + is OpenTimer -> findNavController().navigate( + R.id.activeTimerFragment, + Bundle().apply { putLong("timerId", state.timerId) } + ) + } + }) + } + coroutineScope!!.launch { + timerListViewModel.init() + } + } + + private fun renderLoading() { + addTimerButton.hide() + error.visibility = View.GONE + progressBar.visibility = View.VISIBLE + timerList.visibility = View.GONE + } + + private fun renderEmptyList() { + addTimerButton.show() + error.visibility = View.VISIBLE + error.text = "Add a new timer to get started." + progressBar.visibility = View.GONE + timerList.visibility = View.GONE + } + + private fun renderSuccessState(timers: List) { + addTimerButton.show() + error.visibility = View.GONE + progressBar.visibility = View.GONE + timerList.visibility = View.VISIBLE + (timerList.adapter as? TimerListAdapter)?.submitList(timers) + } + + private fun renderErrorState(message: String) { + addTimerButton.hide() + error.visibility = View.VISIBLE + error.text = message + progressBar.visibility = View.GONE + timerList.visibility = View.GONE + } + + override fun onDestroyView() { + coroutineScope?.cancel() + coroutineScope = null + super.onDestroyView() + } +} diff --git a/app/src/main/java/com/wbrawner/trainterval/timerlist/TimerListViewModel.kt b/app/src/main/java/com/wbrawner/trainterval/timerlist/TimerListViewModel.kt new file mode 100644 index 0000000..9db9390 --- /dev/null +++ b/app/src/main/java/com/wbrawner/trainterval/timerlist/TimerListViewModel.kt @@ -0,0 +1,73 @@ +package com.wbrawner.trainterval.timerlist + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.wbrawner.trainterval.Logger +import com.wbrawner.trainterval.model.IntervalTimer +import com.wbrawner.trainterval.model.IntervalTimerDao +import com.wbrawner.trainterval.timerlist.IntervalTimerListState.* +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +class TimerListViewModel( + private val logger: Logger, + val timerDao: IntervalTimerDao +) : ViewModel() { + val timerState: MutableLiveData = MutableLiveData(LoadingState) + private val timers = ArrayList() + private var initialized = false + + fun init() { + if (initialized) return + initialized = true + GlobalScope.launch { + timerDao.getAll() + .collect { + logger.d(message = "Received updated intervaltimer list") + logger.d(message = it.toString()) + timers.clear() + timers.addAll(it) + if (timers.isEmpty()) { + timerState.postValue(EmptyListState) + } else { + timerState.postValue(SuccessListState(timers)) + } + } + } + } + + suspend fun addTimer() { + timerState.postValue(CreateTimer) + } + + suspend fun editTimer(timer: IntervalTimer) { + + } + + suspend fun deleteTimer(timer: IntervalTimer) { + + } + + suspend fun confirmDeleteTimer(timer: IntervalTimer) { + + } + + suspend fun openTimer(timer: IntervalTimer) { + + } +} + +/** + * Used to represent each state on the main list view. + */ +sealed class IntervalTimerListState { + object LoadingState : IntervalTimerListState() + object EmptyListState : IntervalTimerListState() + data class ConfirmDeleteTimerState(val timer: IntervalTimer) : IntervalTimerListState() + data class SuccessListState(val timers: List) : IntervalTimerListState() + data class ErrorState(val message: String) : IntervalTimerListState() + object CreateTimer : IntervalTimerListState() + data class EditTimer(val timer: IntervalTimer) : IntervalTimerListState() + data class OpenTimer(val timerId: Long) : IntervalTimerListState() +} diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_rounded_corners.xml b/app/src/main/res/drawable/background_rounded_corners.xml new file mode 100644 index 0000000..6723407 --- /dev/null +++ b/app/src/main/res/drawable/background_rounded_corners.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 0000000..7aac7c8 --- /dev/null +++ b/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_pause.xml b/app/src/main/res/drawable/ic_pause.xml new file mode 100644 index 0000000..bb28a6c --- /dev/null +++ b/app/src/main/res/drawable/ic_pause.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_play_arrow.xml b/app/src/main/res/drawable/ic_play_arrow.xml new file mode 100644 index 0000000..bf9b895 --- /dev/null +++ b/app/src/main/res/drawable/ic_play_arrow.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_skip_next.xml b/app/src/main/res/drawable/ic_skip_next.xml new file mode 100644 index 0000000..1253ff0 --- /dev/null +++ b/app/src/main/res/drawable/ic_skip_next.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_skip_previous.xml b/app/src/main/res/drawable/ic_skip_previous.xml new file mode 100644 index 0000000..cd05efe --- /dev/null +++ b/app/src/main/res/drawable/ic_skip_previous.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..5badb61 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/duration_picker.xml b/app/src/main/res/layout/duration_picker.xml new file mode 100644 index 0000000..271edae --- /dev/null +++ b/app/src/main/res/layout/duration_picker.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_active_timer.xml b/app/src/main/res/layout/fragment_active_timer.xml new file mode 100644 index 0000000..1bb9bf6 --- /dev/null +++ b/app/src/main/res/layout/fragment_active_timer.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_timer_form.xml b/app/src/main/res/layout/fragment_timer_form.xml new file mode 100644 index 0000000..2efb8e2 --- /dev/null +++ b/app/src/main/res/layout/fragment_timer_form.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_timer_list.xml b/app/src/main/res/layout/fragment_timer_list.xml new file mode 100644 index 0000000..dac45a2 --- /dev/null +++ b/app/src/main/res/layout/fragment_timer_list.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..a571e60 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..61da551 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..c41dd28 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..db5080a Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..6dba46d Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..da31a87 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..15ac681 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..b216f2d Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..f25a419 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..e96783c Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml new file mode 100644 index 0000000..070ef1c --- /dev/null +++ b/app/src/main/res/navigation/nav_graph.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..d4edbda --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,9 @@ + + + #6200EE + #3700B3 + #03DAC5 + #111111 + #FFFFFF + #F1F1F1 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..1c342bb --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,21 @@ + + Trainterval + Timer Name + Timer Description + Low Intensity Duration + High Intensity Duration + Save + Sets + Cycles + Warm-Up Duration + Cool-Down Duration + Repeat + Successfully saved \'%1$s\' timer + : + Skip Previous + Skip Next + Start Timer + Rest Duration + Items + Item Detail + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..34e700c --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,23 @@ + + + + + + + +