diff --git a/RecyclerViewClickHandler/README.md b/RecyclerViewClickHandler/README.md new file mode 100755 index 0000000..2df9584 --- /dev/null +++ b/RecyclerViewClickHandler/README.md @@ -0,0 +1,55 @@ +TrackMySleepQuality with RecyclerView - Solution Code for 7.4 +============================================================= + +Solution code for Android Kotlin Fundamentals Codelab 7.4 Interacting with RecyclerView items + +Introduction +------------ + +TrackMySleepQuality is an app for recording sleep data for each night. +You can record a start and stop time, assign a quality rating, and clear the database. + +Learn how to make items in the RecyclerView clickable. +Implement a click listener and navigate on click in your Android Kotlin app. + +Pre-requisites +-------------- + +You should be familiar with: + +* Building a basic user interface (UI) using an activity, fragments, and views. +* Navigating between fragments, and using safeArgs to pass data between fragments. +* Using view models, view model factories, transformations, and LiveData and their observers. +* Creating a Room database, creating a DAO, and defining entities. +* Using coroutines for database tasks and other long-running tasks. +* How to implement a basic RecyclerView with an Adapter, ViewHolder, and item layout. +* How to implement data binding for RecyclerView. +* How to create and use binding adapters to transform data. +* How to use GridLayoutManager. + + +Getting Started +--------------- + +1. Download and run the app. + +License +------- + +Copyright 2019 Google, Inc. + +Licensed to the Apache Software Foundation (ASF) under one or more contributor +license agreements. See the NOTICE file distributed with this work for +additional information regarding copyright ownership. The ASF licenses this +file to you under the Apache License, Version 2.0 (the "License"); you may not +use this file except in compliance with the License. You may obtain a copy of +the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations under +the License. + diff --git a/RecyclerViewClickHandler/app/.gitignore b/RecyclerViewClickHandler/app/.gitignore new file mode 100755 index 0000000..796b96d --- /dev/null +++ b/RecyclerViewClickHandler/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/RecyclerViewClickHandler/app/build.gradle b/RecyclerViewClickHandler/app/build.gradle new file mode 100755 index 0000000..89f56de --- /dev/null +++ b/RecyclerViewClickHandler/app/build.gradle @@ -0,0 +1,80 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' +apply plugin: 'androidx.navigation.safeargs' + +android { + compileSdkVersion 28 + defaultConfig { + applicationId "com.example.android.trackmysleepqualityrecyclerview" + minSdkVersion 19 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables.useSupportLibrary = true + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + // Enables data binding. + dataBinding { + enabled = true + } + +} + +dependencies { + + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + + // Support libraries + implementation "androidx.appcompat:appcompat:1.0.0" + implementation "androidx.fragment:fragment:1.0.0" + implementation "androidx.constraintlayout:constraintlayout:2.0.0-alpha2" + + // Android KTX + implementation 'androidx.core:core-ktx:1.0.1' + + // Room and Lifecycle dependencies + 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.lifecycle:lifecycle-extensions:2.0.0" + + // Coroutines + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version" + + // Navigation + implementation "android.arch.navigation:navigation-fragment-ktx:$navigationVersion" + implementation "android.arch.navigation:navigation-ui-ktx:$navigationVersion" + + // Testing + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.1.0-alpha4' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha4' +} + diff --git a/RecyclerViewClickHandler/app/proguard-rules.pro b/RecyclerViewClickHandler/app/proguard-rules.pro new file mode 100755 index 0000000..f1b4245 --- /dev/null +++ b/RecyclerViewClickHandler/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/RecyclerViewClickHandler/app/src/main/AndroidManifest.xml b/RecyclerViewClickHandler/app/src/main/AndroidManifest.xml new file mode 100755 index 0000000..58a0b98 --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + diff --git a/RecyclerViewClickHandler/app/src/main/ic_launcher_sleep_tracker-web.png b/RecyclerViewClickHandler/app/src/main/ic_launcher_sleep_tracker-web.png new file mode 100755 index 0000000..668bfae Binary files /dev/null and b/RecyclerViewClickHandler/app/src/main/ic_launcher_sleep_tracker-web.png differ diff --git a/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/MainActivity.kt b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/MainActivity.kt new file mode 100755 index 0000000..f06095d --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/MainActivity.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.trackmysleepquality + +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/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/Util.kt b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/Util.kt new file mode 100755 index 0000000..bf2f542 --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/Util.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.trackmysleepquality + +import android.annotation.SuppressLint +import android.content.res.Resources +import android.os.Build +import android.text.Html +import android.text.Spanned +import android.view.View +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.example.android.trackmysleepquality.database.SleepNight +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.TimeUnit + +/** + * These functions create a formatted string that can be set in a TextView. + */ + +private val ONE_MINUTE_MILLIS = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES) +private val ONE_HOUR_MILLIS = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS) + +/** + * Convert a duration to a formatted string for display. + * + * Examples: + * + * 6 seconds on Wednesday + * 2 minutes on Monday + * 40 hours on Thursday + * + * @param startTimeMilli the start of the interval + * @param endTimeMilli the end of the interval + * @param res resources used to load formatted strings + */ +fun convertDurationToFormatted(startTimeMilli: Long, endTimeMilli: Long, res: Resources): String { + val durationMilli = endTimeMilli - startTimeMilli + val weekdayString = SimpleDateFormat("EEEE", Locale.getDefault()).format(startTimeMilli) + return when { + durationMilli < ONE_MINUTE_MILLIS -> { + val seconds = TimeUnit.SECONDS.convert(durationMilli, TimeUnit.MILLISECONDS) + res.getString(R.string.seconds_length, seconds, weekdayString) + } + durationMilli < ONE_HOUR_MILLIS -> { + val minutes = TimeUnit.MINUTES.convert(durationMilli, TimeUnit.MILLISECONDS) + res.getString(R.string.minutes_length, minutes, weekdayString) + } + else -> { + val hours = TimeUnit.HOURS.convert(durationMilli, TimeUnit.MILLISECONDS) + res.getString(R.string.hours_length, hours, weekdayString) + } + } +} + +/** + * Returns a string representing the numeric quality rating. + */ +fun convertNumericQualityToString(quality: Int, resources: Resources): String { + var qualityString = resources.getString(R.string.three_ok) + when (quality) { + -1 -> qualityString = "--" + 0 -> qualityString = resources.getString(R.string.zero_very_bad) + 1 -> qualityString = resources.getString(R.string.one_poor) + 2 -> qualityString = resources.getString(R.string.two_soso) + 4 -> qualityString = resources.getString(R.string.four_pretty_good) + 5 -> qualityString = resources.getString(R.string.five_excellent) + } + return qualityString +} + + +/** + * Take the Long milliseconds returned by the system and stored in Room, + * and convert it to a nicely formatted string for display. + * + * EEEE - Display the long letter version of the weekday + * MMM - Display the letter abbreviation of the nmotny + * dd-yyyy - day in month and full year numerically + * HH:mm - Hours and minutes in 24hr format + */ +@SuppressLint("SimpleDateFormat") +fun convertLongToDateString(systemTime: Long): String { + return SimpleDateFormat("EEEE MMM-dd-yyyy' Time: 'HH:mm") + .format(systemTime).toString() +} + +/** + * Takes a list of SleepNights and converts and formats it into one string for display. + * + * For display in a TextView, we have to supply one string, and styles are per TextView, not + * applicable per word. So, we build a formatted string using HTML. This is handy, but we will + * learn a better way of displaying this data in a future lesson. + * + * @param nights - List of all SleepNights in the database. + * @param resources - Resources object for all the resources defined for our app. + * + * @return Spanned - An interface for text that has formatting attached to it. + * See: https://developer.android.com/reference/android/text/Spanned + */ +fun formatNights(nights: List, resources: Resources): Spanned { + val sb = StringBuilder() + sb.apply { + append(resources.getString(R.string.title)) + nights.forEach { + append("
") + append(resources.getString(R.string.start_time)) + append("\t${convertLongToDateString(it.startTimeMilli)}
") + if (it.endTimeMilli != it.startTimeMilli) { + append(resources.getString(R.string.end_time)) + append("\t${convertLongToDateString(it.endTimeMilli)}
") + append(resources.getString(R.string.quality)) + append("\t${convertNumericQualityToString(it.sleepQuality, resources)}
") + append(resources.getString(R.string.hours_slept)) + // Hours + append("\t ${it.endTimeMilli.minus(it.startTimeMilli) / 1000 / 60 / 60}:") + // Minutes + append("${it.endTimeMilli.minus(it.startTimeMilli) / 1000 / 60}:") + // Seconds + append("${it.endTimeMilli.minus(it.startTimeMilli) / 1000}

") + } + } + } + // fromHtml is deprecated for target API without a flag, but since our minSDK is 19, we + // can't use the newer version, which requires minSDK of 24 + //https://developer.android.com/reference/android/text/Html#fromHtml(java.lang.String,%20int) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return Html.fromHtml(sb.toString(), Html.FROM_HTML_MODE_LEGACY) + } else { + @Suppress("DEPRECATION") + return Html.fromHtml(sb.toString()) + } +} + +/** + * ViewHolder that holds a single [TextView]. + * + * A ViewHolder holds a view for the [RecyclerView] as well as providing additional information + * to the RecyclerView such as where on the screen it was last drawn during scrolling. + */ +class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView) \ No newline at end of file diff --git a/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/database/SleepDatabase.kt b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/database/SleepDatabase.kt new file mode 100755 index 0000000..483642b --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/database/SleepDatabase.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.trackmysleepquality.database + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +/** + * A database that stores SleepNight information. + * And a global method to get access to the database. + * + * This pattern is pretty much the same for any database, + * so you can reuse it. + */ +@Database(entities = [SleepNight::class], version = 1, exportSchema = false) +abstract class SleepDatabase : RoomDatabase() { + + /** + * Connects the database to the DAO. + */ + abstract val sleepDatabaseDao: SleepDatabaseDao + + /** + * Define a companion object, this allows us to add functions on the SleepDatabase class. + * + * For example, clients can call `SleepDatabase.getInstance(context)` to instantiate + * a new SleepDatabase. + */ + companion object { + /** + * INSTANCE will keep a reference to any database returned via getInstance. + * + * This will help us avoid repeatedly initializing the database, which is expensive. + * + * The value of a volatile variable will never be cached, and all writes and + * reads will be done to and from the main memory. It means that changes made by one + * thread to shared data are visible to other threads. + */ + @Volatile + private var INSTANCE: SleepDatabase? = null + + /** + * Helper function to get the database. + * + * If a database has already been retrieved, the previous database will be returned. + * Otherwise, create a new database. + * + * This function is threadsafe, and callers should cache the result for multiple database + * calls to avoid overhead. + * + * This is an example of a simple Singleton pattern that takes another Singleton as an + * argument in Kotlin. + * + * To learn more about Singleton read the wikipedia article: + * https://en.wikipedia.org/wiki/Singleton_pattern + * + * @param context The application context Singleton, used to get access to the filesystem. + */ + fun getInstance(context: Context): SleepDatabase { + // Multiple threads can ask for the database at the same time, ensure we only initialize + // it once by using synchronized. Only one thread may enter a synchronized block at a + // time. + synchronized(this) { + + // Copy the current value of INSTANCE to a local variable so Kotlin can smart cast. + // Smart cast is only available to local variables. + var instance = INSTANCE + + // If instance is `null` make a new database instance. + if (instance == null) { + instance = Room.databaseBuilder( + context.applicationContext, + SleepDatabase::class.java, + "sleep_history_database" + ) + // Wipes and rebuilds instead of migrating if no Migration object. + // Migration is not part of this lesson. You can learn more about + // migration with Room in this blog post: + // https://medium.com/androiddevelopers/understanding-migrations-with-room-f01e04b07929 + .fallbackToDestructiveMigration() + .build() + // Assign INSTANCE to the newly created database. + INSTANCE = instance + } + + // Return instance; smart cast to be non-null. + return instance + } + } + } +} diff --git a/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/database/SleepDatabaseDao.kt b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/database/SleepDatabaseDao.kt new file mode 100755 index 0000000..39c5444 --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/database/SleepDatabaseDao.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.trackmysleepquality.database + +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update + + +/** + * Defines methods for using the SleepNight class with Room. + */ +@Dao +interface SleepDatabaseDao { + + @Insert + fun insert(night: SleepNight) + + /** + * When updating a row with a value already set in a column, + * replaces the old value with the new one. + * + * @param night new value to write + */ + @Update + fun update(night: SleepNight) + + /** + * Selects and returns the row that matches the supplied start time, which is our key. + * + * @param key startTimeMilli to match + */ + @Query("SELECT * from daily_sleep_quality_table WHERE nightId = :key") + fun get(key: Long): SleepNight + + /** + * Deletes all values from the table. + * + * This does not delete the table, only its contents. + */ + @Query("DELETE FROM daily_sleep_quality_table") + fun clear() + + /** + * Selects and returns all rows in the table, + * + * sorted by start time in descending order. + */ + @Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC") + fun getAllNights(): LiveData> + + /** + * Selects and returns the latest night. + */ + @Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC LIMIT 1") + fun getTonight(): SleepNight? + + /** + * Selects and returns the night with given nightId. + */ + @Query("SELECT * from daily_sleep_quality_table WHERE nightId = :key") + fun getNightWithId(key: Long): LiveData +} diff --git a/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/database/SleepNight.kt b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/database/SleepNight.kt new file mode 100755 index 0000000..b776812 --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/database/SleepNight.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.trackmysleepquality.database + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +/** + * Represents one night's sleep through start, end times, and the sleep quality. + */ +@Entity(tableName = "daily_sleep_quality_table") +data class SleepNight( + @PrimaryKey(autoGenerate = true) + var nightId: Long = 0L, + + @ColumnInfo(name = "start_time_milli") + val startTimeMilli: Long = System.currentTimeMillis(), + + @ColumnInfo(name = "end_time_milli") + var endTimeMilli: Long = startTimeMilli, + + @ColumnInfo(name = "quality_rating") + var sleepQuality: Int = -1) diff --git a/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleepdetail/SleepDetailFragment.kt b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleepdetail/SleepDetailFragment.kt new file mode 100644 index 0000000..81b3310 --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleepdetail/SleepDetailFragment.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.trackmysleepquality.sleepdetail + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import androidx.navigation.fragment.findNavController +import com.example.android.trackmysleepquality.R +import com.example.android.trackmysleepquality.database.SleepDatabase +import com.example.android.trackmysleepquality.databinding.FragmentSleepDetailBinding + + +/** + * A simple [Fragment] subclass. + * Activities that contain this fragment must implement the + * [SleepDetailFragment.OnFragmentInteractionListener] interface + * to handle interaction events. + * Use the [SleepDetailFragment.newInstance] factory method to + * create an instance of this fragment. + * + */ +class SleepDetailFragment : Fragment() { + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + + // Get a reference to the binding object and inflate the fragment views. + val binding: FragmentSleepDetailBinding = DataBindingUtil.inflate( + inflater, R.layout.fragment_sleep_detail, container, false) + + val application = requireNotNull(this.activity).application + val arguments = SleepDetailFragmentArgs.fromBundle(arguments) + + // Create an instance of the ViewModel Factory. + val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao + val viewModelFactory = SleepDetailViewModelFactory(arguments.sleepNightKey, dataSource) + + // Get a reference to the ViewModel associated with this fragment. + val sleepDetailViewModel = + ViewModelProviders.of( + this, viewModelFactory).get(SleepDetailViewModel::class.java) + + // To use the View Model with data binding, you have to explicitly + // give the binding object a reference to it. + binding.sleepDetailViewModel = sleepDetailViewModel + + binding.setLifecycleOwner(this) + + // Add an Observer to the state variable for Navigating when a Quality icon is tapped. + sleepDetailViewModel.navigateToSleepTracker.observe(this, Observer { + if (it == true) { // Observed state is true. + this.findNavController().navigate( + SleepDetailFragmentDirections.actionSleepDetailFragmentToSleepTrackerFragment()) + // Reset state to make sure we only navigate once, even if the device + // has a configuration change. + sleepDetailViewModel.doneNavigating() + } + }) + + return binding.root + } + + +} \ No newline at end of file diff --git a/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleepdetail/SleepDetailViewModel.kt b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleepdetail/SleepDetailViewModel.kt new file mode 100644 index 0000000..e449a94 --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleepdetail/SleepDetailViewModel.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.trackmysleepquality.sleepdetail + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.example.android.trackmysleepquality.database.SleepDatabaseDao +import com.example.android.trackmysleepquality.database.SleepNight +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * ViewModel for SleepQualityFragment. + * + * @param sleepNightKey The key of the current night we are working on. + */ +class SleepDetailViewModel( + private val sleepNightKey: Long = 0L, + dataSource: SleepDatabaseDao) : ViewModel() { + + /** + * Hold a reference to SleepDatabase via its SleepDatabaseDao. + */ + val database = dataSource + + /** Coroutine setup variables */ + + /** + * viewModelJob allows us to cancel all coroutines started by this ViewModel. + */ + private val viewModelJob = Job() + + private val night: LiveData + + fun getNight() = night + + + init { + night=database.getNightWithId(sleepNightKey) + } + + /** + * Variable that tells the fragment whether it should navigate to [SleepTrackerFragment]. + * + * This is `private` because we don't want to expose the ability to set [MutableLiveData] to + * the [Fragment] + */ + private val _navigateToSleepTracker = MutableLiveData() + + /** + * When true immediately navigate back to the [SleepTrackerFragment] + */ + val navigateToSleepTracker: LiveData + get() = _navigateToSleepTracker + + /** + * Cancels all coroutines when the ViewModel is cleared, to cleanup any pending work. + * + * onCleared() gets called when the ViewModel is destroyed. + */ + override fun onCleared() { + super.onCleared() + viewModelJob.cancel() + } + + + /** + * Call this immediately after navigating to [SleepTrackerFragment] + */ + fun doneNavigating() { + _navigateToSleepTracker.value = null + } + + fun onClose() { + _navigateToSleepTracker.value = true + } + +} \ No newline at end of file diff --git a/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleepdetail/SleepDetailViewModelFactory.kt b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleepdetail/SleepDetailViewModelFactory.kt new file mode 100644 index 0000000..fb59a98 --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleepdetail/SleepDetailViewModelFactory.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.trackmysleepquality.sleepdetail + + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.example.android.trackmysleepquality.database.SleepDatabaseDao + +/** + * This is pretty much boiler plate code for a ViewModel Factory. + * + * Provides the key for the night and the SleepDatabaseDao to the ViewModel. + */ +class SleepDetailViewModelFactory( + private val sleepNightKey: Long, + private val dataSource: SleepDatabaseDao) : ViewModelProvider.Factory { + @Suppress("unchecked_cast") + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(SleepDetailViewModel::class.java)) { + return SleepDetailViewModel(sleepNightKey, dataSource) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file diff --git a/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleepquality/SleepQualityFragment.kt b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleepquality/SleepQualityFragment.kt new file mode 100755 index 0000000..db22082 --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleepquality/SleepQualityFragment.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.trackmysleepquality.sleepquality + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import androidx.navigation.fragment.findNavController +import com.example.android.trackmysleepquality.R +import com.example.android.trackmysleepquality.database.SleepDatabase +import com.example.android.trackmysleepquality.databinding.FragmentSleepQualityBinding + +/** + * Fragment that displays a list of clickable icons, + * each representing a sleep quality rating. + * Once the user taps an icon, the quality is set in the current sleepNight + * and the database is updated. + */ +class SleepQualityFragment : Fragment() { + + /** + * Called when the Fragment is ready to display content to the screen. + * + * This function uses DataBindingUtil to inflate R.layout.fragment_sleep_quality. + * + * It is also responsible for passing the [SleepQualityViewModel] to the + * [FragmentSleepQualityBinding] generated by DataBinding. This will allow DataBinding + * to use the [LiveData] on our ViewModel. + */ + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + + // Get a reference to the binding object and inflate the fragment views. + val binding: FragmentSleepQualityBinding = DataBindingUtil.inflate( + inflater, R.layout.fragment_sleep_quality, container, false) + + val application = requireNotNull(this.activity).application + val arguments = SleepQualityFragmentArgs.fromBundle(arguments) + + // Create an instance of the ViewModel Factory. + val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao + val viewModelFactory = SleepQualityViewModelFactory(arguments.sleepNightKey, dataSource) + + // Get a reference to the ViewModel associated with this fragment. + val sleepQualityViewModel = + ViewModelProviders.of( + this, viewModelFactory).get(SleepQualityViewModel::class.java) + + // To use the View Model with data binding, you have to explicitly + // give the binding object a reference to it. + binding.sleepQualityViewModel = sleepQualityViewModel + + // Add an Observer to the state variable for Navigating when a Quality icon is tapped. + sleepQualityViewModel.navigateToSleepTracker.observe(this, Observer { + if (it == true) { // Observed state is true. + this.findNavController().navigate( + SleepQualityFragmentDirections.actionSleepQualityFragmentToSleepTrackerFragment()) + // Reset state to make sure we only navigate once, even if the device + // has a configuration change. + sleepQualityViewModel.doneNavigating() + } + }) + + return binding.root + } +} diff --git a/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleepquality/SleepQualityViewModel.kt b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleepquality/SleepQualityViewModel.kt new file mode 100755 index 0000000..52ce4d8 --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleepquality/SleepQualityViewModel.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.trackmysleepquality.sleepquality + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.example.android.trackmysleepquality.database.SleepDatabaseDao +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * ViewModel for SleepQualityFragment. + * + * @param sleepNightKey The key of the current night we are working on. + */ +class SleepQualityViewModel( + private val sleepNightKey: Long = 0L, + dataSource: SleepDatabaseDao) : ViewModel() { + + /** + * Hold a reference to SleepDatabase via its SleepDatabaseDao. + */ + val database = dataSource + + /** Coroutine setup variables */ + + /** + * viewModelJob allows us to cancel all coroutines started by this ViewModel. + */ + private val viewModelJob = Job() + + /** + * A [CoroutineScope] keeps track of all coroutines started by this ViewModel. + * + * Because we pass it [viewModelJob], any coroutine started in this scope can be cancelled + * by calling `viewModelJob.cancel()` + * + * By default, all coroutines started in uiScope will launch in [Dispatchers.Main] which is + * the main thread on Android. This is a sensible default because most coroutines started by + * a [ViewModel] update the UI after performing some processing. + */ + private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob) + + /** + * Variable that tells the fragment whether it should navigate to [SleepTrackerFragment]. + * + * This is `private` because we don't want to expose the ability to set [MutableLiveData] to + * the [Fragment] + */ + private val _navigateToSleepTracker = MutableLiveData() + + /** + * When true immediately navigate back to the [SleepTrackerFragment] + */ + val navigateToSleepTracker: LiveData + get() = _navigateToSleepTracker + + /** + * Cancels all coroutines when the ViewModel is cleared, to cleanup any pending work. + * + * onCleared() gets called when the ViewModel is destroyed. + */ + override fun onCleared() { + super.onCleared() + viewModelJob.cancel() + } + + /** + * Call this immediately after navigating to [SleepTrackerFragment] + */ + fun doneNavigating() { + _navigateToSleepTracker.value = null + } + + /** + * Sets the sleep quality and updates the database. + * + * Then navigates back to the SleepTrackerFragment. + */ + fun onSetSleepQuality(quality: Int) { + uiScope.launch { + // IO is a thread pool for running operations that access the disk, such as + // our Room database. + withContext(Dispatchers.IO) { + val tonight = database.get(sleepNightKey) + tonight.sleepQuality = quality + database.update(tonight) + } + + // Setting this state variable to true will alert the observer and trigger navigation. + _navigateToSleepTracker.value = true + } + } +} diff --git a/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleepquality/SleepQualityViewModelFactory.kt b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleepquality/SleepQualityViewModelFactory.kt new file mode 100755 index 0000000..3c6a35d --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleepquality/SleepQualityViewModelFactory.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.trackmysleepquality.sleepquality + + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.example.android.trackmysleepquality.database.SleepDatabaseDao + +/** + * This is pretty much boiler plate code for a ViewModel Factory. + * + * Provides the key for the night and the SleepDatabaseDao to the ViewModel. + */ +class SleepQualityViewModelFactory( + private val sleepNightKey: Long, + private val dataSource: SleepDatabaseDao) : ViewModelProvider.Factory { + @Suppress("unchecked_cast") + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(SleepQualityViewModel::class.java)) { + return SleepQualityViewModel(sleepNightKey, dataSource) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} diff --git a/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleeptracker/BindingUtils.kt b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleeptracker/BindingUtils.kt new file mode 100644 index 0000000..b5089e0 --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleeptracker/BindingUtils.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.trackmysleepquality.sleeptracker + +import android.widget.ImageView +import android.widget.TextView +import androidx.databinding.BindingAdapter +import com.example.android.trackmysleepquality.R +import com.example.android.trackmysleepquality.convertDurationToFormatted +import com.example.android.trackmysleepquality.convertNumericQualityToString +import com.example.android.trackmysleepquality.database.SleepNight + + +@BindingAdapter("sleepDurationFormatted") +fun TextView.setSleepDurationFormatted(item: SleepNight?) { + item?.let { + text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, context.resources) + } +} + + +@BindingAdapter("sleepQualityString") +fun TextView.setSleepQualityString(item: SleepNight?) { + item?.let { + text = convertNumericQualityToString(item.sleepQuality, context.resources) + } +} + + +@BindingAdapter("sleepImage") +fun ImageView.setSleepImage(item: SleepNight?) { + item?.let { + setImageResource(when (item.sleepQuality) { + 0 -> R.drawable.ic_sleep_0 + 1 -> R.drawable.ic_sleep_1 + 2 -> R.drawable.ic_sleep_2 + + 3 -> R.drawable.ic_sleep_3 + + 4 -> R.drawable.ic_sleep_4 + 5 -> R.drawable.ic_sleep_5 + else -> R.drawable.ic_sleep_active + }) + } +} \ No newline at end of file diff --git a/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleeptracker/SleepNightAdapter.kt b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleeptracker/SleepNightAdapter.kt new file mode 100644 index 0000000..9382360 --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleeptracker/SleepNightAdapter.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.trackmysleepquality.sleeptracker + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.example.android.trackmysleepquality.database.SleepNight +import com.example.android.trackmysleepquality.databinding.ListItemSleepNightBinding + +class SleepNightAdapter(val clickListener: SleepNightListener) : ListAdapter(SleepNightDiffCallback()) { + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(getItem(position)!!, clickListener) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder.from(parent) + } + + class ViewHolder private constructor(val binding: ListItemSleepNightBinding) : RecyclerView.ViewHolder(binding.root){ + + fun bind(item: SleepNight, clickListener: SleepNightListener) { + binding.sleep = item + binding.clickListener = clickListener + binding.executePendingBindings() + } + + companion object { + fun from(parent: ViewGroup): ViewHolder { + val layoutInflater = LayoutInflater.from(parent.context) + val binding = ListItemSleepNightBinding.inflate(layoutInflater, parent, false) + return ViewHolder(binding) + } + } + } +} + + +class SleepNightDiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean { + return oldItem.nightId == newItem.nightId + } + + override fun areContentsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean { + return oldItem == newItem + } +} + + +class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) { + fun onClick(night: SleepNight) = clickListener(night.nightId) +} diff --git a/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleeptracker/SleepTrackerFragment.kt b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleeptracker/SleepTrackerFragment.kt new file mode 100755 index 0000000..10d7d34 --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleeptracker/SleepTrackerFragment.kt @@ -0,0 +1,140 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.trackmysleepquality.sleeptracker + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.GridLayoutManager +import com.example.android.trackmysleepquality.R +import com.example.android.trackmysleepquality.database.SleepDatabase +import com.example.android.trackmysleepquality.databinding.FragmentSleepTrackerBinding +import com.google.android.material.snackbar.Snackbar + +/** + * A fragment with buttons to record start and end times for sleep, which are saved in + * a database. Cumulative data is displayed in a simple scrollable TextView. + * (Because we have not learned about RecyclerView yet.) + * The Clear button will clear all data from the database. + */ +class SleepTrackerFragment : Fragment() { + + /** + * Called when the Fragment is ready to display content to the screen. + * + * This function uses DataBindingUtil to inflate R.layout.fragment_sleep_quality. + * + * It is also responsible for passing the [SleepTrackerViewModel] to the + * [FragmentSleepTrackerBinding] generated by DataBinding. This will allow DataBinding + * to use the [LiveData] on our ViewModel. + */ + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + + // Get a reference to the binding object and inflate the fragment views. + val binding: FragmentSleepTrackerBinding = DataBindingUtil.inflate( + inflater, R.layout.fragment_sleep_tracker, container, false) + + val application = requireNotNull(this.activity).application + + // Create an instance of the ViewModel Factory. + val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao + val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application) + + // Get a reference to the ViewModel associated with this fragment. + val sleepTrackerViewModel = + ViewModelProviders.of( + this, viewModelFactory).get(SleepTrackerViewModel::class.java) + + // To use the View Model with data binding, you have to explicitly + // give the binding object a reference to it. + binding.sleepTrackerViewModel = sleepTrackerViewModel + + val adapter = SleepNightAdapter(SleepNightListener { nightId -> + //Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show() + sleepTrackerViewModel.onSleepNightClicked(nightId) + }) + binding.sleepList.adapter = adapter + + + sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer { + it?.let { + adapter.submitList(it) + } + }) + + // Specify the current activity as the lifecycle owner of the binding. + // This is necessary so that the binding can observe LiveData updates. + binding.setLifecycleOwner(this) + + // Add an Observer on the state variable for showing a Snackbar message + // when the CLEAR button is pressed. + sleepTrackerViewModel.showSnackBarEvent.observe(this, Observer { + if (it == true) { // Observed state is true. + Snackbar.make( + activity!!.findViewById(android.R.id.content), + getString(R.string.cleared_message), + Snackbar.LENGTH_SHORT // How long to display the message. + ).show() + // Reset state to make sure the toast is only shown once, even if the device + // has a configuration change. + sleepTrackerViewModel.doneShowingSnackbar() + } + }) + + // Add an Observer on the state variable for Navigating when STOP button is pressed. + sleepTrackerViewModel.navigateToSleepQuality.observe(this, Observer { night -> + night?.let { + // We need to get the navController from this, because button is not ready, and it + // just has to be a view. For some reason, this only matters if we hit stop again + // after using the back button, not if we hit stop and choose a quality. + // Also, in the Navigation Editor, for Quality -> Tracker, check "Inclusive" for + // popping the stack to get the correct behavior if we press stop multiple times + // followed by back. + // Also: https://stackoverflow.com/questions/28929637/difference-and-uses-of-oncreate-oncreateview-and-onactivitycreated-in-fra + this.findNavController().navigate( + SleepTrackerFragmentDirections + .actionSleepTrackerFragmentToSleepQualityFragment(night.nightId)) + // Reset state to make sure we only navigate once, even if the device + // has a configuration change. + sleepTrackerViewModel.doneNavigating() + } + }) + + // Add an Observer on the state variable for Navigating when and item is clicked. + sleepTrackerViewModel.navigateToSleepDetail.observe(this, Observer { night -> + night?.let { + + this.findNavController().navigate( + SleepTrackerFragmentDirections + .actionSleepTrackerFragmentToSleepDetailFragment(night)) + sleepTrackerViewModel.onSleepDetailNavigated() + } + }) + + val manager = GridLayoutManager(activity, 3) + binding.sleepList.layoutManager = manager + + return binding.root + } +} diff --git a/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleeptracker/SleepTrackerViewModel.kt b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleeptracker/SleepTrackerViewModel.kt new file mode 100755 index 0000000..a42e69f --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleeptracker/SleepTrackerViewModel.kt @@ -0,0 +1,263 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.trackmysleepquality.sleeptracker + +import android.app.Application +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations +import androidx.lifecycle.ViewModel +import com.example.android.trackmysleepquality.database.SleepDatabaseDao +import com.example.android.trackmysleepquality.database.SleepNight +import com.example.android.trackmysleepquality.formatNights +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * ViewModel for SleepTrackerFragment. + */ +class SleepTrackerViewModel( + dataSource: SleepDatabaseDao, + application: Application) : ViewModel() { + + /** + * Hold a reference to SleepDatabase via SleepDatabaseDao. + */ + val database = dataSource + + /** Coroutine variables */ + + /** + * viewModelJob allows us to cancel all coroutines started by this ViewModel. + */ + private var viewModelJob = Job() + + /** + * A [CoroutineScope] keeps track of all coroutines started by this ViewModel. + * + * Because we pass it [viewModelJob], any coroutine started in this uiScope can be cancelled + * by calling `viewModelJob.cancel()` + * + * By default, all coroutines started in uiScope will launch in [Dispatchers.Main] which is + * the main thread on Android. This is a sensible default because most coroutines started by + * a [ViewModel] update the UI after performing some processing. + */ + private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob) + + private var tonight = MutableLiveData() + + val nights = database.getAllNights() + + /** + * Converted nights to Spanned for displaying. + */ + val nightsString = Transformations.map(nights) { nights -> + formatNights(nights, application.resources) + } + + /** + * If tonight has not been set, then the START button should be visible. + */ + val startButtonVisible = Transformations.map(tonight) { + null == it + } + + /** + * If tonight has been set, then the STOP button should be visible. + */ + val stopButtonVisible = Transformations.map(tonight) { + null != it + } + + /** + * If there are any nights in the database, show the CLEAR button. + */ + val clearButtonVisible = Transformations.map(nights) { + it?.isNotEmpty() + } + + /** + * Request a toast by setting this value to true. + * + * This is private because we don't want to expose setting this value to the Fragment. + */ + private var _showSnackbarEvent = MutableLiveData() + + /** + * If this is true, immediately `show()` a toast and call `doneShowingSnackbar()`. + */ + val showSnackBarEvent: LiveData + get() = _showSnackbarEvent + + /** + * Variable that tells the Fragment to navigate to a specific [SleepQualityFragment] + * + * This is private because we don't want to expose setting this value to the Fragment. + */ + private val _navigateToSleepQuality = MutableLiveData() + + /** + * If this is non-null, immediately navigate to [SleepQualityFragment] and call [doneNavigating] + */ + val navigateToSleepQuality: LiveData + get() = _navigateToSleepQuality + + /** + * Call this immediately after calling `show()` on a toast. + * + * It will clear the toast request, so if the user rotates their phone it won't show a duplicate + * toast. + */ + fun doneShowingSnackbar() { + _showSnackbarEvent.value = null + } + + /** + * Call this immediately after navigating to [SleepQualityFragment] + * + * It will clear the navigation request, so if the user rotates their phone it won't navigate + * twice. + */ + fun doneNavigating() { + _navigateToSleepQuality.value = null + } + + /** + * Navigation for the SleepDetail fragment. + */ + private val _navigateToSleepDetail = MutableLiveData() + val navigateToSleepDetail + get() = _navigateToSleepDetail + + fun onSleepNightClicked(id: Long) { + _navigateToSleepDetail.value = id + } + + fun onSleepDetailNavigated() { + _navigateToSleepDetail.value = null + } + + init { + initializeTonight() + } + + private fun initializeTonight() { + uiScope.launch { + tonight.value = getTonightFromDatabase() + } + } + + /** + * Handling the case of the stopped app or forgotten recording, + * the start and end times will be the same.j + * + * If the start time and end time are not the same, then we do not have an unfinished + * recording. + */ + private suspend fun getTonightFromDatabase(): SleepNight? { + return withContext(Dispatchers.IO) { + var night = database.getTonight() + if (night?.endTimeMilli != night?.startTimeMilli) { + night = null + } + night + } + } + + private suspend fun insert(night: SleepNight) { + withContext(Dispatchers.IO) { + database.insert(night) + } + } + + private suspend fun update(night: SleepNight) { + withContext(Dispatchers.IO) { + database.update(night) + } + } + + private suspend fun clear() { + withContext(Dispatchers.IO) { + database.clear() + } + } + + /** + * Executes when the START button is clicked. + */ + fun onStart() { + uiScope.launch { + // Create a new night, which captures the current time, + // and insert it into the database. + val newNight = SleepNight() + + insert(newNight) + + tonight.value = getTonightFromDatabase() + } + } + + /** + * Executes when the STOP button is clicked. + */ + fun onStop() { + uiScope.launch { + // In Kotlin, the return@label syntax is used for specifying which function among + // several nested ones this statement returns from. + // In this case, we are specifying to return from launch(). + val oldNight = tonight.value ?: return@launch + + // Update the night in the database to add the end time. + oldNight.endTimeMilli = System.currentTimeMillis() + + update(oldNight) + + // Set state to navigate to the SleepQualityFragment. + _navigateToSleepQuality.value = oldNight + } + } + + /** + * Executes when the CLEAR button is clicked. + */ + fun onClear() { + uiScope.launch { + // Clear the database table. + clear() + + // And clear tonight since it's no longer in the database + tonight.value = null + + // Show a snackbar message, because it's friendly. + _showSnackbarEvent.value = true + } + } + + /** + * Called when the ViewModel is dismantled. + * At this point, we want to cancel all coroutines; + * otherwise we end up with processes that have nowhere to return to + * using memory and resources. + */ + override fun onCleared() { + super.onCleared() + viewModelJob.cancel() + } +} \ No newline at end of file diff --git a/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleeptracker/SleepTrackerViewModelFactory.kt b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleeptracker/SleepTrackerViewModelFactory.kt new file mode 100755 index 0000000..abcd8ac --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/java/com/example/android/trackmysleepquality/sleeptracker/SleepTrackerViewModelFactory.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.trackmysleepquality.sleeptracker + + +import android.app.Application +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.example.android.trackmysleepquality.database.SleepDatabaseDao + +/** + * This is pretty much boiler plate code for a ViewModel Factory. + * + * Provides the SleepDatabaseDao and context to the ViewModel. + */ +class SleepTrackerViewModelFactory( + private val dataSource: SleepDatabaseDao, + private val application: Application) : ViewModelProvider.Factory { + @Suppress("unchecked_cast") + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(SleepTrackerViewModel::class.java)) { + return SleepTrackerViewModel(dataSource, application) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} diff --git a/RecyclerViewClickHandler/app/src/main/res/anim/slide_in_right.xml b/RecyclerViewClickHandler/app/src/main/res/anim/slide_in_right.xml new file mode 100755 index 0000000..49f7779 --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/res/anim/slide_in_right.xml @@ -0,0 +1,25 @@ + + + + + + \ No newline at end of file diff --git a/RecyclerViewClickHandler/app/src/main/res/drawable/ic_launcher_sleep_tracker_background.xml b/RecyclerViewClickHandler/app/src/main/res/drawable/ic_launcher_sleep_tracker_background.xml new file mode 100755 index 0000000..0b1f6bd --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/res/drawable/ic_launcher_sleep_tracker_background.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + diff --git a/RecyclerViewClickHandler/app/src/main/res/drawable/ic_launcher_sleep_tracker_foreground.xml b/RecyclerViewClickHandler/app/src/main/res/drawable/ic_launcher_sleep_tracker_foreground.xml new file mode 100755 index 0000000..a117d56 --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/res/drawable/ic_launcher_sleep_tracker_foreground.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/RecyclerViewClickHandler/app/src/main/res/drawable/ic_sleep_0.xml b/RecyclerViewClickHandler/app/src/main/res/drawable/ic_sleep_0.xml new file mode 100755 index 0000000..91c0b47 --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/res/drawable/ic_sleep_0.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + diff --git a/RecyclerViewClickHandler/app/src/main/res/drawable/ic_sleep_1.xml b/RecyclerViewClickHandler/app/src/main/res/drawable/ic_sleep_1.xml new file mode 100755 index 0000000..8cc34f7 --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/res/drawable/ic_sleep_1.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/RecyclerViewClickHandler/app/src/main/res/drawable/ic_sleep_2.xml b/RecyclerViewClickHandler/app/src/main/res/drawable/ic_sleep_2.xml new file mode 100755 index 0000000..0b7b818 --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/res/drawable/ic_sleep_2.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/RecyclerViewClickHandler/app/src/main/res/drawable/ic_sleep_3.xml b/RecyclerViewClickHandler/app/src/main/res/drawable/ic_sleep_3.xml new file mode 100755 index 0000000..677b9ae --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/res/drawable/ic_sleep_3.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/RecyclerViewClickHandler/app/src/main/res/drawable/ic_sleep_4.xml b/RecyclerViewClickHandler/app/src/main/res/drawable/ic_sleep_4.xml new file mode 100755 index 0000000..39eab52 --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/res/drawable/ic_sleep_4.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/RecyclerViewClickHandler/app/src/main/res/drawable/ic_sleep_5.xml b/RecyclerViewClickHandler/app/src/main/res/drawable/ic_sleep_5.xml new file mode 100755 index 0000000..f23a2ae --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/res/drawable/ic_sleep_5.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + diff --git a/RecyclerViewClickHandler/app/src/main/res/drawable/ic_sleep_active.xml b/RecyclerViewClickHandler/app/src/main/res/drawable/ic_sleep_active.xml new file mode 100755 index 0000000..a117d56 --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/res/drawable/ic_sleep_active.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/RecyclerViewClickHandler/app/src/main/res/font/roboto.xml b/RecyclerViewClickHandler/app/src/main/res/font/roboto.xml new file mode 100755 index 0000000..2641caf --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/res/font/roboto.xml @@ -0,0 +1,7 @@ + + + diff --git a/RecyclerViewClickHandler/app/src/main/res/layout/activity_main.xml b/RecyclerViewClickHandler/app/src/main/res/layout/activity_main.xml new file mode 100755 index 0000000..a21679c --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/RecyclerViewClickHandler/app/src/main/res/layout/fragment_sleep_detail.xml b/RecyclerViewClickHandler/app/src/main/res/layout/fragment_sleep_detail.xml new file mode 100644 index 0000000..a4a70ca --- /dev/null +++ b/RecyclerViewClickHandler/app/src/main/res/layout/fragment_sleep_detail.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + +