commit 37c23ccb26f8686ef0feeee71d6fa548d5717d8a Author: Wyatt J. Miller Date: Tue Nov 16 22:01:38 2021 -0500 add all the things diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..603b140 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..a832f01 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Exam2 \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..3cc336b --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+ + +
+
\ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..9202674 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,22 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..a5f05cd --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..37a7509 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/render.experimental.xml b/.idea/render.experimental.xml new file mode 100644 index 0000000..8ec256a --- /dev/null +++ b/.idea/render.experimental.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..ac7933b --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,41 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 30 + buildToolsVersion "30.0.2" + + defaultConfig { + applicationId "com.wyattjmiller.exam2" + minSdkVersion 24 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'androidx.core:core-ktx:1.3.2' + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation 'com.android.support:support-v4:24.1.1' + implementation 'androidx.navigation:navigation-fragment-ktx:2.1.0' + implementation 'androidx.navigation:navigation-ui-ktx:2.1.0' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/wyattjmiller/exam2/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/wyattjmiller/exam2/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..bf4c489 --- /dev/null +++ b/app/src/androidTest/java/com/wyattjmiller/exam2/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.wyattjmiller.exam2 + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.wyattjmiller.exam2", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5fb1233 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/exam2/AboutActivity.kt b/app/src/main/java/com/wyattjmiller/exam2/AboutActivity.kt new file mode 100644 index 0000000..1b8a19f --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/exam2/AboutActivity.kt @@ -0,0 +1,23 @@ +package com.wyattjmiller.exam2 + +import android.content.SharedPreferences +import android.os.Bundle +import android.preference.PreferenceManager +import androidx.appcompat.app.AppCompatActivity + +class AboutActivity : AppCompatActivity() { + private var mSharedPrefs: SharedPreferences? = null + private var mDarkTheme = false + + override fun onCreate(savedInstanceState: Bundle?) { + mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this) + mDarkTheme = mSharedPrefs!!.getBoolean(SettingsFragment.PREFERENCE_THEME, false) + + if (mDarkTheme) { + setTheme(R.style.DarkTheme) + } + + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_about) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/exam2/AboutFragment.kt b/app/src/main/java/com/wyattjmiller/exam2/AboutFragment.kt new file mode 100644 index 0000000..74f9248 --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/exam2/AboutFragment.kt @@ -0,0 +1,16 @@ +package com.wyattjmiller.exam2 + +import android.app.Fragment +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView + +class AboutFragment : Fragment() { + private lateinit var mAboutTextView: TextView + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_about, container, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/exam2/Food.kt b/app/src/main/java/com/wyattjmiller/exam2/Food.kt new file mode 100644 index 0000000..151beaf --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/exam2/Food.kt @@ -0,0 +1,8 @@ +package com.wyattjmiller.exam2 + +class Food { + var id: Int? = 0 + var foodRestaurant: String? = null + var foodWebsite: String? = null + var foodFavorite: String? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/exam2/FoodContext.kt b/app/src/main/java/com/wyattjmiller/exam2/FoodContext.kt new file mode 100644 index 0000000..a2eec2d --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/exam2/FoodContext.kt @@ -0,0 +1,8 @@ +package com.wyattjmiller.exam2 + +enum class FoodContext { + ADD, + EDIT, + VIEW, + DELETE +} \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/exam2/FoodDatabase.kt b/app/src/main/java/com/wyattjmiller/exam2/FoodDatabase.kt new file mode 100644 index 0000000..37821af --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/exam2/FoodDatabase.kt @@ -0,0 +1,110 @@ +package com.wyattjmiller.exam2 +import android.content.ContentValues +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import java.util.* + +class FoodDatabase(context: Context?) : SQLiteOpenHelper(context, DATABASE_NAME, null, VERSION) { + override fun onCreate(db: SQLiteDatabase) { + db.execSQL( + "create table " + FoodTable.TABLE + " (" + + FoodTable.COL_ID + " integer primary key autoincrement, " + + FoodTable.COL_RESTAURANT + " text, " + + FoodTable.COL_WEBSITE + " text, " + + FoodTable.COL_FAVORITEFOOD + " text)" + ) + } + + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + db.execSQL("drop table if exists " + FoodTable.TABLE) + onCreate(db) + } + + override fun onOpen(db: SQLiteDatabase) { + super.onOpen(db) + if (!db.isReadOnly) { + // Enable foreign key constraints + db.setForeignKeyConstraintsEnabled(true) + } + } + + fun getFoods(): MutableList { + val foods: MutableList = ArrayList() + val db = this.readableDatabase + val sql = + "select * from " + FoodTable.TABLE + val cursor = db.rawQuery(sql, null) + + if (cursor.moveToFirst()) { + do { + val food = Food() + food.id = cursor.getInt(0) + food.foodRestaurant = cursor.getString(1) + food.foodWebsite = cursor.getString(2) + food.foodFavorite = cursor.getString(3) + foods.add(food) + } while (cursor.moveToNext()) + } + + cursor.close() + return foods + } + + fun getFood(courseid: Int): Food { + var food = Food() + val db = this.readableDatabase + val sql = + "select * from " + FoodTable.TABLE + + " where " + FoodTable.COL_ID + " = ?" + val cursor = db.rawQuery(sql, arrayOf(java.lang.Float.toString(courseid.toFloat()))) + + if (cursor.moveToFirst()) { + food.id = cursor.getInt(0) + food.foodRestaurant = cursor.getString(1) + food.foodWebsite = cursor.getString(2) + food.foodFavorite = cursor.getString(3) + } + + cursor.close() + return food + } + + fun addFood(food: Food): Boolean { + val db = writableDatabase + val values = ContentValues() + values.put(FoodTable.COL_RESTAURANT, food.foodRestaurant) + values.put(FoodTable.COL_WEBSITE, food.foodWebsite) + values.put(FoodTable.COL_FAVORITEFOOD, food.foodFavorite) + val id = db.insert(FoodTable.TABLE, null, values) + food.id = id.toInt() + return id != -1L + } + + fun updateFood(food: Food) { + val db = writableDatabase + val values = ContentValues() + values.put(FoodTable.COL_RESTAURANT, food.foodRestaurant) + values.put(FoodTable.COL_WEBSITE, food.foodWebsite) + values.put(FoodTable.COL_FAVORITEFOOD, food.foodFavorite) + db.update(FoodTable.TABLE, values, FoodTable.COL_ID + " = " + food.id, null) + } + + fun deleteFood(food: Food) { + val db = writableDatabase + db.delete(FoodTable.TABLE, FoodTable.COL_RESTAURANT + " = ?", arrayOf(food.foodRestaurant)) + } + + companion object { + private const val DATABASE_NAME = "courses.db" + private const val VERSION = 1 + private var mFoodDb: FoodDatabase? = null + + fun getInstance(context: Context?): FoodDatabase { + if (mFoodDb == null) { + mFoodDb = FoodDatabase(context) + } + return mFoodDb!! + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/exam2/FoodDialogFragment.kt b/app/src/main/java/com/wyattjmiller/exam2/FoodDialogFragment.kt new file mode 100644 index 0000000..aff3cff --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/exam2/FoodDialogFragment.kt @@ -0,0 +1,148 @@ +package com.wyattjmiller.exam2 + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.widget.Button +import android.widget.EditText +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment + +class FoodDialogFragment : DialogFragment() { + private var mTitleTextView: TextView? = null + private lateinit var mRestaurantTextView: TextView + private lateinit var mWebsiteTextView: TextView + private lateinit var mFavoriteFoodTextView: TextView + private lateinit var mRestaurantEditText: EditText + private lateinit var mWebsiteEditText: EditText + private lateinit var mFavoriteFoodEditText: EditText + private lateinit var mConfirmButton: Button + private var mFood: Food = Food() + private lateinit var mFoodDb: FoodDatabase + private lateinit var mFoodContext: FoodContext + + interface onCourseEnteredListener { + fun onCourseEntered(food: Food) + } + + private var mListener: onCourseEnteredListener? = null + + + override fun onAttach(context: Context) { + super.onAttach(context) + mListener = context as onCourseEnteredListener + } + + override fun onCreateDialog(savedInstanceState: Bundle?) : Dialog { + val builder = AlertDialog.Builder(this.activity!!) + val dialogView = activity!!.layoutInflater.inflate(R.layout.activity_main_edit, null) + + mTitleTextView = dialogView.findViewById(R.id.textViewTitle) + + mRestaurantTextView = dialogView.findViewById(R.id.textViewRestaurant) + mWebsiteTextView = dialogView.findViewById(R.id.textViewWebiste) + mFavoriteFoodTextView = dialogView.findViewById(R.id.textViewFavoriteFood) + + mRestaurantEditText = dialogView.findViewById(R.id.editTextRestaurant) + mWebsiteEditText = dialogView.findViewById(R.id.editTextWebsite) + mFavoriteFoodEditText = dialogView.findViewById(R.id.editTextFavoriteFood) + + mConfirmButton = dialogView.findViewById(R.id.buttonConfirm) + + when (mFoodContext) { + FoodContext.ADD -> { + mTitleTextView?.text = getString(R.string.food_title_add) + mConfirmButton.text = getString(R.string.food_add) + } + FoodContext.EDIT -> { + val food = mFoodDb.getFood(mFood.id!!) + + mTitleTextView?.text = getString(R.string.food_title_edit) + mConfirmButton.text = getString(R.string.food_update) + + mRestaurantEditText.setText(food.foodRestaurant) + mWebsiteEditText.setText(food.foodWebsite) + mFavoriteFoodEditText.setText(food.foodFavorite) + } + FoodContext.VIEW -> { + val food = mFoodDb.getFood(mFood.id!!) + + mTitleTextView?.text = getString(R.string.food_title_view) + mConfirmButton.text = getString(R.string.food_view) + + mRestaurantEditText.setText(food.foodRestaurant) + mWebsiteEditText.setText(food.foodWebsite) + mFavoriteFoodEditText.setText(food.foodFavorite) + + mRestaurantEditText.setTextIsSelectable(false) + mWebsiteEditText.setTextIsSelectable(false) + mFavoriteFoodEditText.setTextIsSelectable(false) + } + } + + builder.setView(dialogView) + + mConfirmButton.setOnClickListener { + when (mFoodContext) { + FoodContext.ADD -> { + mFood.foodRestaurant = mRestaurantEditText.text.toString() + mFood.foodWebsite = mWebsiteEditText.text.toString() + mFood.foodFavorite = mFavoriteFoodEditText.text.toString() + + if (validityCheck()) { + mListener?.onCourseEntered(mFood) + dismiss() + } + } + FoodContext.EDIT -> { + mFood.foodRestaurant = mRestaurantEditText.text.toString() + mFood.foodWebsite = mWebsiteEditText.text.toString() + mFood.foodFavorite = mFavoriteFoodEditText.text.toString() + + if (validityCheck()) { + mListener?.onCourseEntered(mFood) + dismiss() + } + } + FoodContext.VIEW -> { +// mRestaurantEditText + dismiss() + } + } + } + + return builder.create() + } + + private fun validityCheck(): Boolean { + val regex = "^((https?|ftp|smtp):\\/\\/)?(www.)?[a-z0-9]+\\.[a-z]+(\\/[a-zA-Z0-9#]+\\/?)*\$".toRegex() + return if (mRestaurantEditText?.text.toString() == "" || + mWebsiteEditText?.text.toString() == "" || + mFavoriteFoodEditText?.text.toString() == "") { + Toast.makeText(this.activity!!, "Please fill in all of the fields", Toast.LENGTH_LONG).show() + false + } else { + if (regex.matches(mWebsiteEditText?.text.toString())) true else { + Toast.makeText(this.activity!!, "Is this a URL?", Toast.LENGTH_LONG).show() + false + } + } + } + + fun getContext(context: FoodContext, database: FoodDatabase) { + mFoodContext = context + mFoodDb = database + } + + fun getFoodId(foodId: Int) { + mFood?.id = foodId + } + + companion object { + + + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/exam2/FoodTable.kt b/app/src/main/java/com/wyattjmiller/exam2/FoodTable.kt new file mode 100644 index 0000000..304a941 --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/exam2/FoodTable.kt @@ -0,0 +1,9 @@ +package com.wyattjmiller.exam2 + +object FoodTable { + const val TABLE = "food" + const val COL_ID = "_id" + const val COL_RESTAURANT = "restaurant" + const val COL_WEBSITE = "website" + const val COL_FAVORITEFOOD = "favoritefood" +} \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/exam2/MainActivity.kt b/app/src/main/java/com/wyattjmiller/exam2/MainActivity.kt new file mode 100644 index 0000000..9d501b7 --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/exam2/MainActivity.kt @@ -0,0 +1,251 @@ +package com.wyattjmiller.exam2 + +import android.content.Intent +import android.content.SharedPreferences +import android.graphics.Color +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.preference.PreferenceManager +import android.view.* +import android.widget.TextView +import android.widget.Toast +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView + +class MainActivity : AppCompatActivity(), FoodDialogFragment.onCourseEnteredListener { + private var mFoodListRecyclerView: RecyclerView? = null + private var mFoodAdapter: FoodAdapter? = null + private lateinit var mFoodDb: FoodDatabase + private lateinit var mSelectedFood: Food + private var mActionMode: ActionMode? = null + private var mSelectedFoodPosition = RecyclerView.NO_POSITION + private lateinit var mDialog: FoodDialogFragment + private var mSharedPrefs: SharedPreferences? = null + private var mDarkTheme = false + + override fun onCreate(savedInstanceState: Bundle?) { + mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this) + mDarkTheme = mSharedPrefs!!.getBoolean(SettingsFragment.PREFERENCE_THEME, false) + + if (mDarkTheme) setTheme(R.style.DarkTheme) + + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + mFoodListRecyclerView = findViewById(R.id.foodRecyclerView) + mFoodDb = FoodDatabase.getInstance(applicationContext) + + val layoutManager: RecyclerView.LayoutManager = LinearLayoutManager(applicationContext) + mFoodListRecyclerView!!.layoutManager = layoutManager + mFoodAdapter = FoodAdapter(loadFoods()) + mFoodListRecyclerView!!.adapter = mFoodAdapter + } + + override fun onResume() { + super.onResume() + + // if theme is changed, change it here + val darkTheme = mSharedPrefs!!.getBoolean(SettingsFragment.PREFERENCE_THEME, false) + if (darkTheme != mDarkTheme) { + recreate() + } + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + val inflater = menuInflater + inflater.inflate(R.menu.subject_menu, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + // Handle item selection + return when (item.itemId) { + R.id.settings -> { + val intent = Intent(this@MainActivity, SettingsActivity::class.java) + startActivity(intent) + true + } + R.id.about -> { + val intent = Intent(this@MainActivity, AboutActivity::class.java) + startActivity(intent) + true + } + R.id.timer -> { + val intent = Intent(this@MainActivity, TimerActivity::class.java) + startActivity(intent) + true + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onCourseEntered(food: Food) { + if (food != null) { + if (food.id == 0) { + if (mFoodDb.addFood(food)) { + mFoodAdapter?.addCourse(food) + } else { + val message = resources.getString("Already exists!".toInt(), food) + Toast.makeText(this@MainActivity, message, Toast.LENGTH_SHORT).show() + } + } else { + if (food.id != 0) { + mFoodDb.updateFood(food) + mFoodAdapter?.updateCourse(food) + } else { + // ?? + } + } + } + } + + fun addFoodClick(view: View?) { + mDialog = FoodDialogFragment() + mDialog.getContext(FoodContext.ADD, mFoodDb) + mDialog.show(supportFragmentManager, "") + } + + private fun loadFoods() : MutableList { + return mFoodDb.getFoods() + } + + + private inner class FoodHolder(inflater: LayoutInflater, parent: ViewGroup?) : RecyclerView.ViewHolder(inflater.inflate(R.layout.recycler_view_items, parent, false)), View.OnClickListener, View.OnLongClickListener { + private var mFood: Food? = null + private val mTextView: TextView + + fun bind(food: Food) { + mFood = food + mTextView.text = food.foodRestaurant + mTextView.setBackgroundColor(Color.LTGRAY) + } + + override fun onClick(view: View) { + // TODO + mDialog = FoodDialogFragment() + mDialog.getContext(FoodContext.VIEW, mFoodDb) + mDialog.getFoodId(mFood?.id!!) + mDialog.show(supportFragmentManager, "") + } + + override fun onLongClick(view: View?): Boolean { + if (mActionMode != null) { + return false + } + + mSelectedFood = mFood!! + mSelectedFoodPosition = adapterPosition + + // Re-bind the selected item + mFoodAdapter?.notifyItemChanged(mSelectedFoodPosition) + + // Show the CAB + mActionMode = this@MainActivity.startActionMode(mActionModeCallback)!! + return true + } + + private val mActionModeCallback: ActionMode.Callback = object : ActionMode.Callback { + override fun onCreateActionMode(mode: ActionMode, menu: Menu?): Boolean { + // Provide context menu for CAB + val inflater = mode.menuInflater + inflater.inflate(R.menu.context_menu, menu) + return true + } + + override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { + return false + } + + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + // Process action item selection + return when (item.itemId) { + R.id.delete -> { + // Delete from the database and remove from the RecyclerView + mFoodDb.deleteFood(mSelectedFood) + mFoodAdapter?.removeCourse(mSelectedFood) + + // Close the CAB + mode.finish() + true + } + R.id.edit -> { + // TODO + // up comes a dialog box editing the desired list item + mFoodAdapter?.takeCourseIndex() + mDialog = FoodDialogFragment() + mDialog.getContext(FoodContext.EDIT, mFoodDb) + mDialog.getFoodId(mSelectedFood.id!!) + mDialog.show(supportFragmentManager, "") + true + } + else -> false + } + } + + override fun onDestroyActionMode(mode: ActionMode) { + mActionMode = null + + // CAB closing, need to deselect item if not deleted + mFoodAdapter?.notifyItemChanged(mSelectedFoodPosition) + mSelectedFoodPosition = RecyclerView.NO_POSITION + } + } + + init { + itemView.setOnClickListener(this) + itemView.setOnLongClickListener(this) + mTextView = itemView.findViewById(R.id.foodTextView) + } + } + + private inner class FoodAdapter(private val mCourseList: MutableList) : RecyclerView.Adapter() { + private var mOldCourseIndex = 0 + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FoodHolder { + val layoutInflater = LayoutInflater.from(applicationContext) + return FoodHolder(layoutInflater, parent) + } + + override fun onBindViewHolder(holder: FoodHolder, position: Int) { + holder.bind(mCourseList[position]) + } + + override fun getItemCount(): Int { + return mCourseList.size + } + + fun takeCourseIndex() { + val index = mSelectedFoodPosition + mOldCourseIndex = index + } + + fun removeCourse(course: Food?) { + // Find subject in the list + val index = mCourseList.indexOf(course) + if (index >= 0) { + // Remove the subject + mCourseList.removeAt(index) + + // Notify adapter of subject removal + notifyItemRemoved(index) + } + } + + fun addCourse(course: Food?) { + // Add the new subject at the beginning of the list + mCourseList.add(0, course!!) + + // Notify the adapter that item was added to the beginning of the list + notifyItemInserted(0) + + // Scroll to the top + mFoodListRecyclerView?.scrollToPosition(0) + } + + fun updateCourse(course: Food?) { + mCourseList.removeAt(mOldCourseIndex) + mCourseList.add(0, course!!) + notifyItemChanged(0) + mFoodListRecyclerView?.scrollToPosition(0) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/exam2/SettingsActivity.java b/app/src/main/java/com/wyattjmiller/exam2/SettingsActivity.java new file mode 100644 index 0000000..23f8b6a --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/exam2/SettingsActivity.java @@ -0,0 +1,28 @@ +package com.wyattjmiller.exam2; + +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceManager; + +import androidx.appcompat.app.AppCompatActivity; + +public class SettingsActivity extends AppCompatActivity { + private SharedPreferences mSharedPrefs; + private boolean mDarkTheme; + + @Override + protected void onCreate(Bundle savedInstanceState) { + mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); + mDarkTheme = mSharedPrefs.getBoolean(SettingsFragment.PREFERENCE_THEME, false); + if (mDarkTheme) { + setTheme(R.style.DarkTheme); + } + + super.onCreate(savedInstanceState); + + // Display the fragment as the main content + getFragmentManager().beginTransaction() + .replace(android.R.id.content, new SettingsFragment()) + .commit(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/exam2/SettingsFragment.java b/app/src/main/java/com/wyattjmiller/exam2/SettingsFragment.java new file mode 100644 index 0000000..a28832b --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/exam2/SettingsFragment.java @@ -0,0 +1,23 @@ +package com.wyattjmiller.exam2; + +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; + +public class SettingsFragment extends PreferenceFragment { + + public static String PREFERENCE_THEME = "pref_theme"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.preferences); + + // Access the default shared prefs + SharedPreferences sharedPrefs = + PreferenceManager.getDefaultSharedPreferences(getActivity()); + } +} diff --git a/app/src/main/java/com/wyattjmiller/exam2/Timer.kt b/app/src/main/java/com/wyattjmiller/exam2/Timer.kt new file mode 100644 index 0000000..eab73d6 --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/exam2/Timer.kt @@ -0,0 +1,83 @@ +package com.wyattjmiller.exam2 + +import android.os.SystemClock +import kotlin.math.max +import kotlin.math.min + +class Timer { + private var mTargetTime: Long = 0 + private var mTimeLeft: Long = 0 + var isRunning = false + private set + private var mDurationMillis: Long = 0 + + fun start(millisLeft: Long) { + mDurationMillis = millisLeft + mTargetTime = SystemClock.uptimeMillis() + mDurationMillis + isRunning = true + } + + fun start(hours: Int, minutes: Int, seconds: Int) { + // Add 1 sec to duration so timer stays on current second longer + mDurationMillis = (hours * 60 * 60 + minutes * 60 + seconds + 1) * 1000.toLong() + mTargetTime = SystemClock.uptimeMillis() + mDurationMillis + isRunning = true + } + + fun stop() { + isRunning = false + } + + fun pause() { + mTimeLeft = mTargetTime - SystemClock.uptimeMillis() + isRunning = false + } + + fun resume() { + mTargetTime = SystemClock.uptimeMillis() + mTimeLeft + isRunning = true + } + + val remainingMilliseconds: Long + get() = if (isRunning) { + max(0, mTargetTime - SystemClock.uptimeMillis()) + } else 0 + + private val remainingSeconds: Int + get() { + return if (isRunning) { + ((remainingMilliseconds / 1000) % 60).toInt() + } else 0 + } + + private val remainingMinutes: Int + get() { + return if (isRunning) { + (((remainingMilliseconds / 1000) / 60) % 60).toInt() + } else 0 + } + + private val remainingHours: Int + get() { + return if (isRunning) { + (((remainingMilliseconds / 1000) / 60) / 60).toInt() + } else 0 + } + + val progressPercent: Int + get() { + return if (mDurationMillis != 1000L) { + min( + 100, 100 - ((remainingMilliseconds - 1000) * 100 / + (mDurationMillis - 1000)).toInt() + ) + } else 0 + } + + override fun toString(): String { + return String.format( + "%02d:%02d:%02d", remainingHours, + remainingMinutes, remainingSeconds + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/exam2/TimerActivity.kt b/app/src/main/java/com/wyattjmiller/exam2/TimerActivity.kt new file mode 100644 index 0000000..26fbadd --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/exam2/TimerActivity.kt @@ -0,0 +1,160 @@ +package com.wyattjmiller.exam2 + +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.os.Handler +import android.preference.PreferenceManager +import android.view.View +import android.widget.Button +import android.widget.NumberPicker +import android.widget.ProgressBar +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import java.text.DecimalFormat + + +class TimerActivity : AppCompatActivity() { + private var mHoursPicker: NumberPicker? = null + private var mMinutesPicker: NumberPicker? = null + private var mSecondsPicker: NumberPicker? = null + private var mStartButton: Button? = null + private var mPauseButton: Button? = null + private var mCancelButton: Button? = null + private var mProgressBar: ProgressBar? = null + private var mTimeLeftTextView: TextView? = null + private var mHandler: Handler? = null + private var mTimerModel: Timer? = null + private var mSharedPrefs: SharedPreferences? = null + private var mDarkTheme: Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this) + mDarkTheme = mSharedPrefs!!.getBoolean(SettingsFragment.PREFERENCE_THEME, false) + + if (mDarkTheme) setTheme(R.style.DarkTheme) + + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_timer) + + // Initially hide the timer and progress bar + mTimeLeftTextView = findViewById(R.id.timeLeft) + mTimeLeftTextView?.visibility = View.INVISIBLE + mProgressBar = findViewById(R.id.progressBar) + mProgressBar?.visibility = View.INVISIBLE + mStartButton = findViewById(R.id.startButton) + mPauseButton = findViewById(R.id.pauseButton) + mCancelButton = findViewById(R.id.cancelButton) + + // Hide pause and cancel buttons until the timer starts + mPauseButton?.visibility = View.GONE + mCancelButton?.visibility = View.GONE + + // Show 2 digits in NumberPickers + val numFormat = + NumberPicker.Formatter { i -> DecimalFormat("00").format(i.toLong()) } + + // Set min and max values for all NumberPickers + mHoursPicker = findViewById(R.id.hoursPicker) + mHoursPicker?.minValue = 0 + mHoursPicker?.maxValue = 99 + mHoursPicker?.setFormatter(numFormat) + mMinutesPicker = findViewById(R.id.minutesPicker) + mMinutesPicker?.minValue = 0 + mMinutesPicker?.maxValue = 59 + mMinutesPicker?.setFormatter(numFormat) + mSecondsPicker = findViewById(R.id.secondsPicker) + mSecondsPicker?.minValue = 0 + mSecondsPicker?.maxValue = 59 + mSecondsPicker?.setFormatter(numFormat) + mTimerModel = Timer() + + // Instantiate default Handler so Runnables can be posted to UI message queue + mHandler = Handler() + } + + override fun onStop() { + super.onStop() + + // Start the service if the timer is running + if (mTimerModel!!.isRunning) { + val intent = Intent(this, TimerIntentService::class.java) + intent.putExtra(TimerIntentService.EXTRA_MILLIS_LEFT, mTimerModel?.remainingMilliseconds) + startService(intent) + } + } + + fun startButtonClick(view: View?) { + // Get values from NumberPickers + val hours = mHoursPicker!!.value + val minutes = mMinutesPicker!!.value + val seconds = mSecondsPicker!!.value + if (hours + minutes + seconds > 0) { + // Show progress + mTimeLeftTextView!!.visibility = View.VISIBLE + mProgressBar!!.progress = 0 + mProgressBar!!.visibility = View.VISIBLE + + // Show only Pause and Cancel buttons + mStartButton!!.visibility = View.GONE + mPauseButton!!.visibility = View.VISIBLE + mPauseButton!!.setText(R.string.pause) + mCancelButton!!.visibility = View.VISIBLE + + // Start the model + mTimerModel?.start(hours, minutes, seconds) + + // Start sending Runnables to message queue + mHandler!!.post(mUpdateTimerRunnable) + } + } + + fun pauseButtonClick(view: View?) { + if (mTimerModel?.isRunning!!) { + // Pause and change to resume button + mTimerModel?.pause() + mHandler!!.removeCallbacks(mUpdateTimerRunnable) + mPauseButton!!.setText(R.string.resume) + } else { + // Resume and change to pause button + mTimerModel?.resume() + mHandler!!.post(mUpdateTimerRunnable) + mPauseButton!!.setText(R.string.pause) + } + } + + fun cancelButtonClick(view: View?) { + mTimeLeftTextView!!.visibility = View.INVISIBLE + mProgressBar!!.visibility = View.INVISIBLE + timerCompleted() + } + + private fun timerCompleted() { + mTimerModel?.stop() + + // Remove any remaining Runnables that may reside in UI message queue + mHandler!!.removeCallbacks(mUpdateTimerRunnable) + + // Show only the start button + mStartButton!!.visibility = View.VISIBLE + mPauseButton!!.visibility = View.GONE + mCancelButton!!.visibility = View.GONE + } + + private val mUpdateTimerRunnable: Runnable = object : Runnable { + override fun run() { + + // Update UI to show remaining time and progress + mTimeLeftTextView?.text = mTimerModel.toString() + val progress: Int = mTimerModel!!.progressPercent + mProgressBar!!.progress = progress + + // Only post Runnable if more time remains + if (progress == 100) { + timerCompleted() + } else { + mHandler!!.postDelayed(this, 200) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/exam2/TimerFragment.kt b/app/src/main/java/com/wyattjmiller/exam2/TimerFragment.kt new file mode 100644 index 0000000..1829574 --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/exam2/TimerFragment.kt @@ -0,0 +1,23 @@ +package com.wyattjmiller.exam2 + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup + +class TimerFragment : Fragment() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_timer, container, false) + } + + companion object { + // ?? + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/exam2/TimerIntentService.kt b/app/src/main/java/com/wyattjmiller/exam2/TimerIntentService.kt new file mode 100644 index 0000000..19fd067 --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/exam2/TimerIntentService.kt @@ -0,0 +1,65 @@ +package com.wyattjmiller.exam2 + +import android.app.IntentService +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Intent +import android.os.Build +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat + +class TimerIntentService : IntentService("TimerIntentService") { + override fun onHandleIntent(intent: Intent?) { + + // Get millis from the activity and start a new TimerModel + val millisLeft = intent!!.getLongExtra(EXTRA_MILLIS_LEFT, 0) + val timerModel = Timer() + timerModel.start(millisLeft) + + // Create notification channel + createTimerNotificationChannel() + while (timerModel.isRunning) { + try { + createTimerNotification(timerModel.toString()) + Thread.sleep(1000) + if (timerModel.remainingMilliseconds === 0L) { + timerModel.stop() + createTimerNotification("Timer is finished!") + } + } catch (e: InterruptedException) { + } + } + } + + private fun createTimerNotificationChannel() { + if (Build.VERSION.SDK_INT < 26) return + val name: CharSequence = getString(R.string.channel_description_time) + val description = getString(R.string.channel_name_time) + val importance = NotificationManager.IMPORTANCE_LOW + val channel = NotificationChannel(CHANNEL_ID_TIMER, name, importance) + channel.description = description + + val notificationManager = getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(channel) + } + + + private fun createTimerNotification(text: String) { + val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID_TIMER) + .setSmallIcon(R.drawable.timer) + .setContentTitle(getString(R.string.app_name)) + .setContentText(text) + .setPriority(NotificationCompat.PRIORITY_LOW) + .build() + + val notificationManager = NotificationManagerCompat.from(this) + notificationManager.notify(NOTIFICATION_ID, notification) + } + + companion object { + const val EXTRA_MILLIS_LEFT = "com.wyattjmiller.exam2.extra.EXTRA_MILLIS_LEFT" + private const val CHANNEL_ID_TIMER = "channel_timer" + private const val NOTIFICATION_ID = 0 + } +} diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/add.xml b/app/src/main/res/drawable/add.xml new file mode 100644 index 0000000..eb23254 --- /dev/null +++ b/app/src/main/res/drawable/add.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/delete.xml b/app/src/main/res/drawable/delete.xml new file mode 100644 index 0000000..3c4030b --- /dev/null +++ b/app/src/main/res/drawable/delete.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/edit.xml b/app/src/main/res/drawable/edit.xml new file mode 100644 index 0000000..2844baf --- /dev/null +++ b/app/src/main/res/drawable/edit.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/help.xml b/app/src/main/res/drawable/help.xml new file mode 100644 index 0000000..c0c9268 --- /dev/null +++ b/app/src/main/res/drawable/help.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/notification.xml b/app/src/main/res/drawable/notification.xml new file mode 100644 index 0000000..6bdced2 --- /dev/null +++ b/app/src/main/res/drawable/notification.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/settings.xml b/app/src/main/res/drawable/settings.xml new file mode 100644 index 0000000..41a82ed --- /dev/null +++ b/app/src/main/res/drawable/settings.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/timer.xml b/app/src/main/res/drawable/timer.xml new file mode 100644 index 0000000..1897572 --- /dev/null +++ b/app/src/main/res/drawable/timer.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml new file mode 100644 index 0000000..bc9d153 --- /dev/null +++ b/app/src/main/res/layout/activity_about.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..a0c6a8e --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main_edit.xml b/app/src/main/res/layout/activity_main_edit.xml new file mode 100644 index 0000000..2983386 --- /dev/null +++ b/app/src/main/res/layout/activity_main_edit.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + +