diff --git a/MADSkillsNavigationSample/.gitignore b/MADSkillsNavigationSample/.gitignore new file mode 100644 index 00000000..09b993d0 --- /dev/null +++ b/MADSkillsNavigationSample/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/MADSkillsNavigationSample/README.md b/MADSkillsNavigationSample/README.md new file mode 100644 index 00000000..e0e919e4 --- /dev/null +++ b/MADSkillsNavigationSample/README.md @@ -0,0 +1,46 @@ +MAD Skills Navigation Sample (Donut Tracker) +============================================== + +This sample shows the features of Navigation component highlighted by the Navigation +episodes in the MAD Skills series of [videos](https://www.youtube.com/user/androiddevelopers) +and [articles](https://medium.com/androiddevelopers). Specifically, episodes +2, 3, andd 4 walk through code from this sample. + +### Features + +This sample showcases the following features of the Navigation component: + + * Dialog destinations (episode 2) + * Using SafeArgs to pass data between destinations (episode 3) + * Navigating via shortcuts and notifications with Deep Links (episode 4) + +### Screenshots +Screenshot + +### Other Resources + + * For an overview of using Navigation component, check out + [Get started with the Navigation component](https://developer.android.com/guide/navigation/navigation-getting-started) + * Consider including the [Navigation KTX libraries](https://developer.android.com/topic/libraries/architecture/adding-components#navigation) + for more concise uses of the Navigation component. For example, calls to `Navigation.findNavController(view)` can + be expressed as `view.findNavController()`. + +License +------- + +Copyright 2020 The Android Open Source Project, 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. \ No newline at end of file diff --git a/MADSkillsNavigationSample/app/build.gradle b/MADSkillsNavigationSample/app/build.gradle new file mode 100644 index 00000000..c15ffd79 --- /dev/null +++ b/MADSkillsNavigationSample/app/build.gradle @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2020 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: "androidx.navigation.safeargs.kotlin" +apply plugin: 'kotlin-kapt' + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.2" + + buildFeatures { + viewBinding = true + } + + defaultConfig { + applicationId "com.android.samples.navdonutcreator" + minSdkVersion 21 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + +// To inline the bytecode built with JVM target 1.8 into +// bytecode that is being built with JVM target 1.6. (e.g. navArgs) + + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + + packagingOptions { + exclude 'META-INF/atomicfu.kotlin_module' + } +} + +configurations { + ktlint +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "androidx.appcompat:appcompat:$appCompatVersion" + implementation "androidx.core:core-ktx:$ktxVersion" + implementation "com.google.android.material:material:$materialVersion" + implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion" + implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion" + implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion" + + implementation "androidx.fragment:fragment-ktx:$fragmentVersion" + + implementation "androidx.lifecycle:lifecycle-livedata-core-ktx:$archLifecycleVersion" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$archLifecycleVersion" + + implementation "androidx.room:room-runtime:$roomVersion" + kapt "androidx.room:room-compiler:$roomVersion" + implementation "androidx.room:room-ktx:$roomVersion" + + api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" + api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" + + ktlint "com.pinterest:ktlint:$ktlintVersion" +} + + +// Disable the 'paren-spacing' rule which conflicts with non-paren annotations on functional types +task ktlint(type: JavaExec, group: "verification") { + description = "Check Kotlin code style." + main = "com.pinterest.ktlint.Main" + classpath = configurations.ktlint + args "src/**/*.kt", "--disabled_rules", "paren-spacing" +} +check.dependsOn ktlint +task ktlintFormat(type: JavaExec, group: "formatting") { + description = "Fix Kotlin code style deviations." + main = "com.pinterest.ktlint.Main" + classpath = configurations.ktlint + args "-F", "src/**/*.kt", "--disabled_rules", "paren-spacing" +} \ No newline at end of file diff --git a/MADSkillsNavigationSample/app/proguard-rules.pro b/MADSkillsNavigationSample/app/proguard-rules.pro new file mode 100644 index 00000000..f1b42451 --- /dev/null +++ b/MADSkillsNavigationSample/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/MADSkillsNavigationSample/app/src/main/AndroidManifest.xml b/MADSkillsNavigationSample/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..b9d491df --- /dev/null +++ b/MADSkillsNavigationSample/app/src/main/AndroidManifest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/Donut.kt b/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/Donut.kt new file mode 100644 index 00000000..59f96eeb --- /dev/null +++ b/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/Donut.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2020 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.android.samples.donuttracker + +import androidx.room.Entity +import androidx.room.PrimaryKey + +/** + * This class holds the data that we are tracking for each donut: its name, a description, and + * a rating. + */ +@Entity +data class Donut( + @PrimaryKey(autoGenerate = true) val id: Long, + val name: String, + val description: String = "", + val rating: Int +) diff --git a/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/DonutEntryDialogFragment.kt b/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/DonutEntryDialogFragment.kt new file mode 100644 index 00000000..3d5c1ee8 --- /dev/null +++ b/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/DonutEntryDialogFragment.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2020 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.android.samples.donuttracker + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.observe +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import com.android.samples.donuttracker.databinding.DonutEntryDialogBinding +import com.android.samples.donuttracker.storage.DonutDatabase +import com.google.android.material.bottomsheet.BottomSheetDialogFragment + +/** + * This dialog allows the user to enter information about a donut, either creating a new + * entry or updating an existing one. + */ +class DonutEntryDialogFragment : BottomSheetDialogFragment() { + + private lateinit var donutEntryViewModel: DonutEntryViewModel + + private enum class EditingState { + NEW_DONUT, + EXISTING_DONUT + } + + @SuppressLint("SetTextI18n") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val donutDao = DonutDatabase.getDatabase(requireContext()).donutDao() + + donutEntryViewModel = ViewModelProvider(this, ViewModelFactory(donutDao)) + .get(DonutEntryViewModel::class.java) + + val binding = DonutEntryDialogBinding.bind(view) + + var donut: Donut? = null + val args: DonutEntryDialogFragmentArgs by navArgs() + val editingState = + if (args.itemId > 0) EditingState.EXISTING_DONUT + else EditingState.NEW_DONUT + + // If we arrived here with an itemId of >= 0, then we are editing an existing item + if (editingState == EditingState.EXISTING_DONUT) { + // Request to edit an existing item, whose id was passed in as an argument. + // Retrieve that item and populate the UI with its details + donutEntryViewModel.get(args.itemId).observe(viewLifecycleOwner) { donutItem -> + binding.name.setText(donutItem.name) + binding.description.setText(donutItem.description) + binding.ratingBar.rating = donutItem.rating.toFloat() + donut = donutItem + } + } + + // When the user clicks the Done button, use the data here to either update + // an existing item or create a new one + binding.doneButton.setOnClickListener { + // Grab these now since the Fragment may go away before the setupNotification + // lambda below is called + val context = requireContext().applicationContext + val navController = findNavController() + + donutEntryViewModel.addData( + donut?.id ?: 0, + binding.name.text.toString(), + binding.description.text.toString(), + binding.ratingBar.rating.toInt() + ) { actualId -> + val arg = DonutEntryDialogFragmentArgs(actualId).toBundle() + val pendingIntent = navController + .createDeepLink() + .setDestination(R.id.donutEntryDialogFragment) + .setArguments(arg) + .createPendingIntent() + + Notifier.postNotification(actualId, context, pendingIntent) + } + dismiss() + } + + // User clicked the Cancel button; just exit the dialog without saving the data + binding.cancelButton.setOnClickListener { + dismiss() + } + } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return DonutEntryDialogBinding.inflate(inflater, container, false).root + } +} diff --git a/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/DonutEntryViewModel.kt b/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/DonutEntryViewModel.kt new file mode 100644 index 00000000..62ee3b68 --- /dev/null +++ b/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/DonutEntryViewModel.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2020 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.android.samples.donuttracker + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.liveData +import androidx.lifecycle.viewModelScope +import com.android.samples.donuttracker.storage.DonutDao +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class DonutEntryViewModel(private val donutDao: DonutDao) : ViewModel() { + + private var donutLiveData: LiveData? = null + + fun get(id: Long): LiveData { + return donutLiveData ?: liveData { + emit(donutDao.get(id)) + }.also { + donutLiveData = it + } + } + + fun addData( + id: Long, + name: String, + description: String, + rating: Int, + setupNotification: (Long) -> Unit + ) { + val donut = Donut(id, name, description, rating) + + CoroutineScope(Dispatchers.Main.immediate).launch { + var actualId = id + + if (id > 0) { + update(donut) + } else { + actualId = insert(donut) + } + + setupNotification(actualId) + } + } + + private suspend fun insert(donut: Donut): Long { + return donutDao.insert(donut) + } + + private fun update(donut: Donut) = viewModelScope.launch(Dispatchers.IO) { + donutDao.update(donut) + } +} diff --git a/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/DonutList.kt b/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/DonutList.kt new file mode 100644 index 00000000..35952d4e --- /dev/null +++ b/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/DonutList.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2020 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.android.samples.donuttracker + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.app.NotificationManagerCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.observe +import androidx.navigation.findNavController +import androidx.navigation.fragment.findNavController +import com.android.samples.donuttracker.databinding.DonutListBinding +import com.android.samples.donuttracker.storage.DonutDatabase +import kotlinx.android.synthetic.main.donut_list.* + +/** + * Fragment containing the RecyclerView which shows the current list of donuts being tracked. + */ +class DonutList : Fragment() { + + private lateinit var donutListViewModel: DonutListViewModel + + private val adapter = DonutListAdapter( + onEdit = { donut -> + findNavController().navigate( + DonutListDirections.actionDonutListToDonutEntryDialogFragment(donut.id) + ) + }, + onDelete = { donut -> + NotificationManagerCompat.from(requireContext()).cancel(donut.id.toInt()) + donutListViewModel.delete(donut) + } + ) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val binding = DonutListBinding.bind(view) + val donutDao = DonutDatabase.getDatabase(requireContext()).donutDao() + donutListViewModel = ViewModelProvider(this, ViewModelFactory(donutDao)) + .get(DonutListViewModel::class.java) + + donutListViewModel.donuts.observe(viewLifecycleOwner) { donuts -> + adapter.submitList(donuts) + } + + recyclerView.adapter = adapter + + binding.fab.setOnClickListener { fabView -> + fabView.findNavController().navigate( + DonutListDirections.actionDonutListToDonutEntryDialogFragment() + ) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return DonutListBinding.inflate(inflater, container, false).root + } +} diff --git a/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/DonutListAdapter.kt b/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/DonutListAdapter.kt new file mode 100644 index 00000000..8c3a821c --- /dev/null +++ b/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/DonutListAdapter.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2020 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.android.samples.donuttracker + +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.android.samples.donuttracker.databinding.DonutItemBinding + +/** + * The adapter used by the RecyclerView to display the current list of donuts + */ +class DonutListAdapter(private var onEdit: (Donut) -> Unit, private var onDelete: (Donut) -> Unit) : + ListAdapter(DonutDiffCallback()) { + + class DonutListViewHolder( + private val binding: DonutItemBinding, + private var onEdit: (Donut) -> Unit, + private var onDelete: (Donut) -> Unit + ) : RecyclerView.ViewHolder(binding.root) { + private var donutId: Long = -1 + private var nameView = binding.name + private var description = binding.description + private var thumbnail = binding.thumbnail + private var rating = binding.rating + private var donut: Donut? = null + + fun bind(donut: Donut) { + donutId = donut.id + nameView.text = donut.name + description.text = donut.description + rating.text = donut.rating.toString() + thumbnail.setImageResource(R.drawable.donut_with_sprinkles) + this.donut = donut + binding.deleteButton.setOnClickListener { + onDelete(donut) + } + binding.root.setOnClickListener { + onEdit(donut) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DonutListViewHolder { + + return DonutListViewHolder( + DonutItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), + onEdit, + onDelete + ) + } + + override fun onBindViewHolder(holder: DonutListViewHolder, position: Int) { + holder.bind(getItem(position)) + } +} + +class DonutDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Donut, newItem: Donut): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: Donut, newItem: Donut): Boolean { + return oldItem == newItem + } +} diff --git a/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/DonutListViewModel.kt b/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/DonutListViewModel.kt new file mode 100644 index 00000000..0e65efe1 --- /dev/null +++ b/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/DonutListViewModel.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2020 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.android.samples.donuttracker + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.android.samples.donuttracker.storage.DonutDao +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +/** + * This ViewModel is used to access the underlying data and to observe changes to it. + */ +class DonutListViewModel(private val donutDao: DonutDao) : ViewModel() { + + // Users of this ViewModel will observe changes to its donuts list to know when + // to redisplay those changes + val donuts: LiveData> = donutDao.getAll() + + fun delete(donut: Donut) = viewModelScope.launch(Dispatchers.IO) { + donutDao.delete(donut) + } +} diff --git a/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/MainActivity.kt b/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/MainActivity.kt new file mode 100644 index 00000000..8ca72eff --- /dev/null +++ b/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/MainActivity.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2020 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.android.samples.donuttracker + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import androidx.appcompat.app.AppCompatActivity +import com.android.samples.donuttracker.databinding.ActivityMainBinding + +/** + * Main activity class. Not much happens here, just some basic UI setup. + * The main logic occurs in the fragments which populate this activity. + */ +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val binding = ActivityMainBinding.inflate(LayoutInflater.from(this)) + setContentView(binding.root) + setSupportActionBar(binding.toolbar) + + Notifier.init(this) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + // Inflate the menu; this adds items to the action bar if it is present. + menuInflater.inflate(R.menu.menu_main, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + return when (item.itemId) { + R.id.action_settings -> true + else -> super.onOptionsItemSelected(item) + } + } +} diff --git a/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/Notifier.kt b/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/Notifier.kt new file mode 100644 index 00000000..1f983fdb --- /dev/null +++ b/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/Notifier.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2020 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.android.samples.donuttracker + +import android.app.Activity +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.os.Build +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat + +/** + * Utility class for posting notifications. + * This class creates the notification channel (as necessary) and posts to it when requested. + */ +object Notifier { + + private const val channelId = "Default" + + fun init(activity: Activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val notificationManager = + activity.getSystemService(AppCompatActivity.NOTIFICATION_SERVICE) as NotificationManager + val existingChannel = notificationManager.getNotificationChannel(channelId) + if (existingChannel == null) { + // Create the NotificationChannel + val name = activity.getString(R.string.defaultChannel) + val importance = NotificationManager.IMPORTANCE_DEFAULT + val mChannel = NotificationChannel(channelId, name, importance) + mChannel.description = activity.getString(R.string.notificationDescription) + notificationManager.createNotificationChannel(mChannel) + } + } + } + + fun postNotification(id: Long, context: Context, intent: PendingIntent) { + val builder = NotificationCompat.Builder(context, channelId) + builder.setContentTitle(context.getString(R.string.deepLinkNotificationTitle)) + .setSmallIcon(R.drawable.donut_with_sprinkles) + val text = context.getString(R.string.addDonutInfo) + val notification = builder.setContentText(text) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setContentIntent(intent) + .setAutoCancel(true) + .build() + val notificationManager = NotificationManagerCompat.from(context) + // Remove prior notifications; only allow one at a time to edit the latest item + notificationManager.cancelAll() + notificationManager.notify(id.toInt(), notification) + } +} diff --git a/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/ViewModelFactory.kt b/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/ViewModelFactory.kt new file mode 100644 index 00000000..5c87c736 --- /dev/null +++ b/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/ViewModelFactory.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2020 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.android.samples.donuttracker + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.android.samples.donuttracker.storage.DonutDao + +class ViewModelFactory(private val donutDao: DonutDao) : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(DonutListViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return DonutListViewModel(donutDao) as T + } else if (modelClass.isAssignableFrom(DonutEntryViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return DonutEntryViewModel(donutDao) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} diff --git a/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/storage/DonutDao.kt b/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/storage/DonutDao.kt new file mode 100644 index 00000000..b6857d81 --- /dev/null +++ b/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/storage/DonutDao.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2020 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.android.samples.donuttracker.storage + +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import com.android.samples.donuttracker.Donut + +/** + * The Data Access Object used to retrieve and store data from/to the underlying database. + * This API is not used directly; instead, callers should use the Repository which calls into + * this DAO. + */ +@Dao +interface DonutDao { + @Query("SELECT * FROM donut") + fun getAll(): LiveData> + + @Query("SELECT * FROM donut WHERE id = :id") + suspend fun get(id: Long): Donut + + @Insert + suspend fun insert(donut: Donut): Long + + @Delete + suspend fun delete(donut: Donut) + + @Update + suspend fun update(donut: Donut) +} diff --git a/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/storage/DonutDatabase.kt b/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/storage/DonutDatabase.kt new file mode 100644 index 00000000..91fbe2bc --- /dev/null +++ b/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/storage/DonutDatabase.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2020 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.android.samples.donuttracker.storage + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import com.android.samples.donuttracker.Donut + +/** + * The underlying database where information about the donuts is stored. + */ +@Database(entities = arrayOf(Donut::class), version = 1) +abstract class DonutDatabase : RoomDatabase() { + + abstract fun donutDao(): DonutDao + + companion object { + @Volatile private var INSTANCE: DonutDatabase? = null + + fun getDatabase(context: Context): DonutDatabase { + val tempInstance = + INSTANCE + if (tempInstance != null) { + return tempInstance + } + synchronized(this) { + val instance = Room.databaseBuilder( + context, + DonutDatabase::class.java, + "donut_database" + ).build() + INSTANCE = instance + return instance + } + } + } +} diff --git a/MADSkillsNavigationSample/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/MADSkillsNavigationSample/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..81a0b812 --- /dev/null +++ b/MADSkillsNavigationSample/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/MADSkillsNavigationSample/app/src/main/res/drawable/donut_with_sprinkles.xml b/MADSkillsNavigationSample/app/src/main/res/drawable/donut_with_sprinkles.xml new file mode 100644 index 00000000..6bcafd9f --- /dev/null +++ b/MADSkillsNavigationSample/app/src/main/res/drawable/donut_with_sprinkles.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MADSkillsNavigationSample/app/src/main/res/drawable/ic_clear_24px.xml b/MADSkillsNavigationSample/app/src/main/res/drawable/ic_clear_24px.xml new file mode 100644 index 00000000..8336dad4 --- /dev/null +++ b/MADSkillsNavigationSample/app/src/main/res/drawable/ic_clear_24px.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/MADSkillsNavigationSample/app/src/main/res/drawable/ic_launcher_background.xml b/MADSkillsNavigationSample/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..9d462630 --- /dev/null +++ b/MADSkillsNavigationSample/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MADSkillsNavigationSample/app/src/main/res/layout/activity_main.xml b/MADSkillsNavigationSample/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..e1e57d75 --- /dev/null +++ b/MADSkillsNavigationSample/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/MADSkillsNavigationSample/app/src/main/res/layout/content_main.xml b/MADSkillsNavigationSample/app/src/main/res/layout/content_main.xml new file mode 100644 index 00000000..305fe587 --- /dev/null +++ b/MADSkillsNavigationSample/app/src/main/res/layout/content_main.xml @@ -0,0 +1,34 @@ + + + + + + diff --git a/MADSkillsNavigationSample/app/src/main/res/layout/donut_entry_dialog.xml b/MADSkillsNavigationSample/app/src/main/res/layout/donut_entry_dialog.xml new file mode 100644 index 00000000..80720353 --- /dev/null +++ b/MADSkillsNavigationSample/app/src/main/res/layout/donut_entry_dialog.xml @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + +