From 1c659c995a75b5d3d424e9744596dde4c0774194 Mon Sep 17 00:00:00 2001 From: "Wyatt J. Miller" Date: Tue, 16 Nov 2021 22:04:40 -0500 Subject: [PATCH] add all the things --- .gitignore | 14 + .idea/codeStyles/Project.xml | 138 ++++++++++ .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/gradle.xml | 22 ++ .idea/jarRepositories.xml | 25 ++ .idea/misc.xml | 9 + .idea/runConfigurations.xml | 12 + app/.gitignore | 1 + app/build.gradle | 45 ++++ app/proguard-rules.pro | 21 ++ .../studyhelper/ExampleInstrumentedTest.kt | 24 ++ app/src/main/AndroidManifest.xml | 38 +++ .../studyhelper/ImportActivity.java | 99 +++++++ .../com/wyattjmiller/studyhelper/Question.kt | 30 +++ .../studyhelper/QuestionActivity.kt | 240 +++++++++++++++++ .../wyattjmiller/studyhelper/QuestionDao.kt | 21 ++ .../studyhelper/QuestionEditActivity.kt | 66 +++++ .../studyhelper/ReceivedListener.kt | 11 + .../studyhelper/SettingsActivity.java | 17 ++ .../studyhelper/SettingsFragment.java | 50 ++++ .../studyhelper/StudyDatabase.java | 229 ++++++++++++++++ .../studyhelper/StudyFetcher.java | 138 ++++++++++ .../com/wyattjmiller/studyhelper/Subject.kt | 24 ++ .../studyhelper/SubjectActivity.kt | 254 ++++++++++++++++++ .../wyattjmiller/studyhelper/SubjectDao.kt | 24 ++ .../studyhelper/SubjectDialogFragment.java | 45 ++++ .../drawable-v24/ic_launcher_foreground.xml | 30 +++ app/src/main/res/drawable/add.xml | 10 + app/src/main/res/drawable/check.xml | 10 + app/src/main/res/drawable/delete.xml | 11 + .../res/drawable/ic_launcher_background.xml | 170 ++++++++++++ .../main/res/drawable/import_questions.xml | 10 + app/src/main/res/drawable/next.xml | 10 + app/src/main/res/drawable/previous.xml | 10 + app/src/main/res/drawable/settings.xml | 10 + app/src/main/res/layout/activity_import.xml | 55 ++++ app/src/main/res/layout/activity_question.xml | 104 +++++++ .../res/layout/activity_question_edit.xml | 67 +++++ app/src/main/res/layout/activity_settings.xml | 9 + app/src/main/res/layout/activity_subject.xml | 27 ++ .../main/res/layout/recycler_view_items.xml | 22 ++ app/src/main/res/menu/context_menu.xml | 11 + app/src/main/res/menu/question_menu.xml | 26 ++ app/src/main/res/menu/subject_menu.xml | 15 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3593 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5339 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2636 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3388 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4926 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7472 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7909 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 11873 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10652 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 16570 bytes app/src/main/res/values/colors.xml | 26 ++ app/src/main/res/values/strings.xml | 48 ++++ app/src/main/res/values/styles.xml | 32 +++ app/src/main/res/xml/preferences.xml | 26 ++ .../studyhelper/ExampleUnitTest.kt | 17 ++ build.gradle | 26 ++ gradle.properties | 21 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 ++++++++++++ gradlew.bat | 84 ++++++ settings.gradle | 2 + 68 files changed, 2679 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/wyattjmiller/studyhelper/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/wyattjmiller/studyhelper/ImportActivity.java create mode 100644 app/src/main/java/com/wyattjmiller/studyhelper/Question.kt create mode 100644 app/src/main/java/com/wyattjmiller/studyhelper/QuestionActivity.kt create mode 100644 app/src/main/java/com/wyattjmiller/studyhelper/QuestionDao.kt create mode 100644 app/src/main/java/com/wyattjmiller/studyhelper/QuestionEditActivity.kt create mode 100644 app/src/main/java/com/wyattjmiller/studyhelper/ReceivedListener.kt create mode 100644 app/src/main/java/com/wyattjmiller/studyhelper/SettingsActivity.java create mode 100644 app/src/main/java/com/wyattjmiller/studyhelper/SettingsFragment.java create mode 100644 app/src/main/java/com/wyattjmiller/studyhelper/StudyDatabase.java create mode 100644 app/src/main/java/com/wyattjmiller/studyhelper/StudyFetcher.java create mode 100644 app/src/main/java/com/wyattjmiller/studyhelper/Subject.kt create mode 100644 app/src/main/java/com/wyattjmiller/studyhelper/SubjectActivity.kt create mode 100644 app/src/main/java/com/wyattjmiller/studyhelper/SubjectDao.kt create mode 100644 app/src/main/java/com/wyattjmiller/studyhelper/SubjectDialogFragment.java create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/add.xml create mode 100644 app/src/main/res/drawable/check.xml create mode 100644 app/src/main/res/drawable/delete.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/import_questions.xml create mode 100644 app/src/main/res/drawable/next.xml create mode 100644 app/src/main/res/drawable/previous.xml create mode 100644 app/src/main/res/drawable/settings.xml create mode 100644 app/src/main/res/layout/activity_import.xml create mode 100644 app/src/main/res/layout/activity_question.xml create mode 100644 app/src/main/res/layout/activity_question_edit.xml create mode 100644 app/src/main/res/layout/activity_settings.xml create mode 100644 app/src/main/res/layout/activity_subject.xml create mode 100644 app/src/main/res/layout/recycler_view_items.xml create mode 100644 app/src/main/res/menu/context_menu.xml create mode 100644 app/src/main/res/menu/question_menu.xml create mode 100644 app/src/main/res/menu/subject_menu.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/xml/preferences.xml create mode 100644 app/src/test/java/com/wyattjmiller/studyhelper/ExampleUnitTest.kt create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle 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/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/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..162a021 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,45 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 29 + buildToolsVersion "30.0.2" + + defaultConfig { + applicationId "com.wyattjmiller.studyhelper" + minSdkVersion 21 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + vectorDrawables.useSupportLibrary = true + 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.3' + implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation 'androidx.cardview:cardview:1.0.0' + implementation 'androidx.vectordrawable:vectordrawable:1.1.0' + implementation 'com.google.android.material:material:1.2.1' + implementation 'com.android.volley:volley:1.1.1' + implementation 'androidx.room:room-runtime:2.2.5' + annotationProcessor 'androidx.room:room-compiler:2.2.5' + 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/studyhelper/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/wyattjmiller/studyhelper/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..d83519b --- /dev/null +++ b/app/src/androidTest/java/com/wyattjmiller/studyhelper/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.wyattjmiller.studyhelper + +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.studyhelper", 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..d899a84 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/studyhelper/ImportActivity.java b/app/src/main/java/com/wyattjmiller/studyhelper/ImportActivity.java new file mode 100644 index 0000000..ebe8928 --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/studyhelper/ImportActivity.java @@ -0,0 +1,99 @@ +package com.wyattjmiller.studyhelper; + +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.CheckBox; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; +import androidx.appcompat.app.AppCompatActivity; +import com.android.volley.VolleyError; +import java.util.List; + +public class ImportActivity extends AppCompatActivity { + + private LinearLayout mSubjectLayoutContainer; + private StudyFetcher mStudyFetcher; + private ProgressBar mLoadingProgressBar; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_import); + + mSubjectLayoutContainer = findViewById(R.id.subjectLayout); + + // Show progress bar + mLoadingProgressBar = findViewById(R.id.loadingProgressBar); + mLoadingProgressBar.setVisibility(View.VISIBLE); + + mStudyFetcher = new StudyFetcher(this); + mStudyFetcher.fetchSubjects(mFetchListener); + } + + private StudyFetcher.OnStudyDataReceivedListener mFetchListener = new StudyFetcher.OnStudyDataReceivedListener() { + @Override + public void onSubjectsReceived(List subjects) { + + // Hide progress bar + mLoadingProgressBar.setVisibility(View.GONE); + + // Create a checkbox for each subject + for (Subject subject: subjects) { + CheckBox checkBox = new CheckBox(getApplicationContext()); + checkBox.setTextSize(24); + checkBox.setText(subject.getText()); + checkBox.setTag(subject); + mSubjectLayoutContainer.addView(checkBox); + } + } + + @Override + public void onQuestionsReceived(List questions) { + + if (questions.size() > 0) { + StudyDatabase studyDb = StudyDatabase.getInstance(getApplicationContext()); + + // Add the questions to the database + for (Question question : questions) { + studyDb.addQuestion(question); + } + + String subject = questions.get(0).getSubject(); + Toast.makeText(getApplicationContext(), subject + " imported successfully", + Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onErrorResponse(VolleyError error) { + Toast.makeText(getApplicationContext(), "Error loading subjects. Try again later.", + Toast.LENGTH_LONG).show(); + mLoadingProgressBar.setVisibility(View.GONE); + Log.d("RESP", error.toString()); + } + }; + + public void importButtonClick(View view) { + StudyDatabase dbHelper = StudyDatabase.getInstance(getApplicationContext()); + + // Determine which subjects were selected + int numCheckBoxes = mSubjectLayoutContainer.getChildCount(); + for (int i = 0; i < numCheckBoxes; i++) { + CheckBox checkBox = (CheckBox) mSubjectLayoutContainer.getChildAt(i); + if (checkBox.isChecked()) { + Subject subject = (Subject) checkBox.getTag(); + + // Add subject to the database + if (dbHelper.addSubject(subject)) { + mStudyFetcher.fetchQuestions(subject, mFetchListener); + } else { + Toast.makeText(this, subject.getText() + " is already imported.", + Toast.LENGTH_SHORT).show(); + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/studyhelper/Question.kt b/app/src/main/java/com/wyattjmiller/studyhelper/Question.kt new file mode 100644 index 0000000..931f638 --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/studyhelper/Question.kt @@ -0,0 +1,30 @@ +package com.wyattjmiller.studyhelper + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey + + +@Entity( + tableName = "questions", + foreignKeys = [ForeignKey( + entity = Subject::class, + parentColumns = ["text"], + childColumns = ["subject"] + )] +) +class Question { + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "id") + var id: Long = 0 + + @ColumnInfo(name = "text") + var text: String? = null + + @ColumnInfo(name = "answer") + var answer: String? = null + + @ColumnInfo(name = "subject") + var subject: String? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/studyhelper/QuestionActivity.kt b/app/src/main/java/com/wyattjmiller/studyhelper/QuestionActivity.kt new file mode 100644 index 0000000..8966ae1 --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/studyhelper/QuestionActivity.kt @@ -0,0 +1,240 @@ +package com.wyattjmiller.studyhelper + +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.snackbar.Snackbar + +open class QuestionActivity : AppCompatActivity() { + private lateinit var mStudyDb: StudyDatabase + private lateinit var mSubject: String + private lateinit var mQuestionList: MutableList + private lateinit var mAnswerLabel: TextView + private lateinit var mAnswerText: TextView + private lateinit var mAnswerButton: Button + private lateinit var mQuestionText: TextView + private lateinit var mShowQuestionsLayout: ViewGroup + private lateinit var mNoQuestionsLayout: ViewGroup + private lateinit var mDeletedQuestion: Question + + private var mCurrentQuestionIndex = 0 + private val REQUEST_CODE_NEW_QUESTION = 0 + private val REQUEST_CODE_UPDATE_QUESTION = 1 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_question) + + // Hosting activity provides the subject of the questions to display + val intent = intent + mSubject = intent.getStringExtra(EXTRA_SUBJECT) + + // Load all questions for this subject + mStudyDb = StudyDatabase.getInstance(applicationContext) + mQuestionList = mStudyDb.getQuestions(mSubject) + mQuestionText = findViewById(R.id.questionText) + mAnswerLabel = findViewById(R.id.answerLabel) + mAnswerText = findViewById(R.id.answerText) + mAnswerButton = findViewById(R.id.answerButton) + mShowQuestionsLayout = findViewById(R.id.showQuestionsLayout) + mNoQuestionsLayout = findViewById(R.id.noQuestionsLayout) + + // Show first question + showQuestion(0) + } + + override fun onStart() { + super.onStart() + + // Are there questions to display? + if (mQuestionList!!.size == 0) { + updateAppBarTitle() + displayQuestion(false) + } else { + displayQuestion(true) + toggleAnswerVisibility() + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + + // Inflate menu for the app bar + val inflater = menuInflater + inflater.inflate(R.menu.question_menu, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + // Determine which app bar item was chosen + return when (item.itemId) { + R.id.previous -> { + showQuestion(mCurrentQuestionIndex - 1) + true + } + R.id.next -> { + showQuestion(mCurrentQuestionIndex + 1) + true + } + R.id.add -> { + addQuestion() + true + } + R.id.edit -> { + editQuestion() + true + } + R.id.delete -> { + deleteQuestion() + true + } + else -> super.onOptionsItemSelected(item) + } + } + + fun addQuestionButtonClick(view: View?) { + addQuestion() + } + + fun answerButtonClick(view: View?) { + toggleAnswerVisibility() + } + + private fun displayQuestion(display: Boolean) { + + // Show or hide the appropriate screen + if (display) { + mShowQuestionsLayout!!.visibility = View.VISIBLE + mNoQuestionsLayout!!.visibility = View.GONE + } else { + mShowQuestionsLayout!!.visibility = View.GONE + mNoQuestionsLayout!!.visibility = View.VISIBLE + } + } + + private fun updateAppBarTitle() { + + // Display subject and number of questions in app bar + val actionBar = supportActionBar + if (actionBar != null) { + val title = resources.getString( + R.string.question_number, + mSubject, mCurrentQuestionIndex + 1, mQuestionList!!.size + ) + setTitle(title) + } + } + + private fun addQuestion() { + val intent = Intent(this, QuestionEditActivity::class.java) + intent.putExtra(QuestionEditActivity.EXTRA_SUBJECT, mSubject) + startActivityForResult(intent, REQUEST_CODE_NEW_QUESTION) + } + + private fun editQuestion() { + if (mCurrentQuestionIndex >= 0) { + val intent = Intent(this, QuestionEditActivity::class.java) + intent.putExtra(Intent.EXTRA_SUBJECT, mSubject) + val questionId = mQuestionList[mCurrentQuestionIndex].id + intent.putExtra(QuestionEditActivity.EXTRA_QUESTION_ID, questionId) + startActivityForResult(intent, REQUEST_CODE_UPDATE_QUESTION) + } + } + + private fun deleteQuestion() { + mDeletedQuestion = mQuestionList[mCurrentQuestionIndex] + mStudyDb.deleteQuestion(mDeletedQuestion.id) + + if (mCurrentQuestionIndex >= 0) { + val questionId = mQuestionList[mCurrentQuestionIndex].id + mStudyDb.deleteQuestion(questionId) + mQuestionList.removeAt(mCurrentQuestionIndex) + if (mQuestionList.size == 0) { + // No questions left to show + mCurrentQuestionIndex = -1 + updateAppBarTitle() + displayQuestion(false) + } else { + showQuestion(mCurrentQuestionIndex) + } + + val snackbar = Snackbar.make(findViewById(R.id.coordinatorLayout), R.string.question_deleted, Snackbar.LENGTH_LONG) + snackbar.setAction(R.string.undo) { + mStudyDb.addQuestion(mDeletedQuestion) + mQuestionList.add(mDeletedQuestion) + showQuestion(mQuestionList.size - 1) + displayQuestion(true) + } + snackbar.show() + } + } + + + private fun showQuestion(questionIndex: Int) { + + // Show question at the given index + var questionIndex = questionIndex + if (mQuestionList!!.size > 0) { + if (questionIndex < 0) { + questionIndex = mQuestionList!!.size - 1 + } else if (questionIndex >= mQuestionList!!.size) { + questionIndex = 0 + } + mCurrentQuestionIndex = questionIndex + updateAppBarTitle() + val question = mQuestionList!![mCurrentQuestionIndex] + mQuestionText!!.text = question.text + mAnswerText!!.text = question.answer + } else { + // No questions yet + mCurrentQuestionIndex = -1 + } + } + + private fun toggleAnswerVisibility() { + if (mAnswerText!!.visibility == View.VISIBLE) { + mAnswerButton!!.setText(R.string.show_answer) + mAnswerText!!.visibility = View.INVISIBLE + mAnswerLabel!!.visibility = View.INVISIBLE + } else { + mAnswerButton!!.setText(R.string.hide_answer) + mAnswerText!!.visibility = View.VISIBLE + mAnswerLabel!!.visibility = View.VISIBLE + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_NEW_QUESTION) { + // Get added question + val questionId = data!!.getLongExtra(QuestionEditActivity.EXTRA_QUESTION_ID, -1) + val newQuestion = mStudyDb.getQuestion(questionId) + + // Add newly created question to the question list and show it + mQuestionList.add(newQuestion) + showQuestion(mQuestionList.size - 1) + Toast.makeText(this, R.string.question_added, Toast.LENGTH_SHORT).show() + } else if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_UPDATE_QUESTION) { + // Get updated question + val questionId = data!!.getLongExtra(QuestionEditActivity.EXTRA_QUESTION_ID, -1) + val updatedQuestion = mStudyDb.getQuestion(questionId) + + // Replace current question in question list with updated question + val currentQuestion = mQuestionList[mCurrentQuestionIndex] + currentQuestion.text = updatedQuestion.text + currentQuestion.answer = updatedQuestion.answer + showQuestion(mCurrentQuestionIndex) + Toast.makeText(this, R.string.question_updated, Toast.LENGTH_SHORT).show() + } + } + + companion object { + const val EXTRA_SUBJECT = "com.wyattjmiller.studyhelper.subject" + } +} diff --git a/app/src/main/java/com/wyattjmiller/studyhelper/QuestionDao.kt b/app/src/main/java/com/wyattjmiller/studyhelper/QuestionDao.kt new file mode 100644 index 0000000..077f321 --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/studyhelper/QuestionDao.kt @@ -0,0 +1,21 @@ +package com.wyattjmiller.studyhelper + +import androidx.room.* + +@Dao +interface QuestionDao { + @Query("SELECT * FROM questions WHERE id = :id") + fun getQuestion(id: Long): Question? + + @Query("SELECT * FROM questions WHERE subject = :subject") + fun getQuestions(subject: String?): List? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertQuestion(question: Question?): Long + + @Update + fun updateQuestion(question: Question?) + + @Delete + fun deleteQuestion(question: Question?) +} diff --git a/app/src/main/java/com/wyattjmiller/studyhelper/QuestionEditActivity.kt b/app/src/main/java/com/wyattjmiller/studyhelper/QuestionEditActivity.kt new file mode 100644 index 0000000..1ae7a5c --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/studyhelper/QuestionEditActivity.kt @@ -0,0 +1,66 @@ +package com.wyattjmiller.studyhelper + +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.EditText +import androidx.appcompat.app.AppCompatActivity + + +class QuestionEditActivity : AppCompatActivity() { + private lateinit var mQuestionText: EditText + private lateinit var mAnswerText: EditText + private lateinit var mStudyDb: StudyDatabase + private lateinit var mQuestion: Question + + private var mQuestionId: Long = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_question_edit) + mQuestionText = findViewById(R.id.questionText) + mAnswerText = findViewById(R.id.answerText) + mStudyDb = StudyDatabase.getInstance(applicationContext) + + // Get question ID from QuestionActivity + val intent = intent + mQuestionId = intent.getLongExtra(EXTRA_QUESTION_ID, -1) + val actionBar = supportActionBar + if (mQuestionId == -1L) { + // Add new question + mQuestion = Question() + setTitle(R.string.add_question) + } else { + // Update existing question + mQuestion = mStudyDb.getQuestion(mQuestionId) + mQuestionText.setText(mQuestion.text) + mAnswerText.setText(mQuestion.answer) + setTitle(R.string.update_question) + } + val subject = intent.getStringExtra(EXTRA_SUBJECT) + mQuestion!!.subject = subject + } + + fun saveButtonClick(view: View?) { + mQuestion!!.text = mQuestionText!!.text.toString() + mQuestion!!.answer = mAnswerText!!.text.toString() + if (mQuestionId == -1L) { + // New question + mStudyDb!!.addQuestion(mQuestion) + } else { + // Existing question + mStudyDb!!.updateQuestion(mQuestion) + } + + // Send back question ID + val intent = Intent() + intent.putExtra(EXTRA_QUESTION_ID, mQuestion!!.id) + setResult(RESULT_OK, intent) + finish() + } + + companion object { + const val EXTRA_QUESTION_ID = "com.zybooks.studyhelper.question_id" + const val EXTRA_SUBJECT = "com.zybooks.studyhelper.subject" + } +} diff --git a/app/src/main/java/com/wyattjmiller/studyhelper/ReceivedListener.kt b/app/src/main/java/com/wyattjmiller/studyhelper/ReceivedListener.kt new file mode 100644 index 0000000..367fbb6 --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/studyhelper/ReceivedListener.kt @@ -0,0 +1,11 @@ +package com.wyattjmiller.studyhelper + +import com.android.volley.VolleyError + +interface ReceivedListener { + interface OnStudyDataReceivedListener { + fun onSubjectsReceived(subjects: List?) + fun onQuestionsReceived(questions: List?) + fun onErrorResponse(error: VolleyError?) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/studyhelper/SettingsActivity.java b/app/src/main/java/com/wyattjmiller/studyhelper/SettingsActivity.java new file mode 100644 index 0000000..ecce2aa --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/studyhelper/SettingsActivity.java @@ -0,0 +1,17 @@ +package com.wyattjmiller.studyhelper; + +import androidx.appcompat.app.AppCompatActivity; +import android.os.Bundle; + +public class SettingsActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + 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/studyhelper/SettingsFragment.java b/app/src/main/java/com/wyattjmiller/studyhelper/SettingsFragment.java new file mode 100644 index 0000000..0bbf738 --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/studyhelper/SettingsFragment.java @@ -0,0 +1,50 @@ +package com.wyattjmiller.studyhelper; + +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; + +public class SettingsFragment extends PreferenceFragment { + + public static String PREFERENCE_THEME = "pref_theme"; + public static String PREFERENCE_SUBJECT_ORDER = "pref_subject_order"; + public static String PREFERENCE_DEFAULT_QUESTION = "pref_default_question"; + + @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()); + + setPrefSummarySubjectOrder(sharedPrefs); + setPrefSummaryDefaultQuestion(sharedPrefs); + } + + // Set the summary to the currently selected subject order + private void setPrefSummarySubjectOrder(SharedPreferences sharedPrefs) { + String order = sharedPrefs.getString(PREFERENCE_SUBJECT_ORDER, "1"); + String[] subjectOrders = getResources().getStringArray(R.array.pref_subject_order); + Preference subjectOrderPref = findPreference(PREFERENCE_SUBJECT_ORDER); + subjectOrderPref.setSummary(subjectOrders[Integer.parseInt(order)]); + } + + // Set the summary to the default question + private void setPrefSummaryDefaultQuestion(SharedPreferences sharedPrefs) { + String defaultQuestion = sharedPrefs.getString(PREFERENCE_DEFAULT_QUESTION, ""); + defaultQuestion = defaultQuestion.trim(); + Preference questionPref = findPreference(PREFERENCE_DEFAULT_QUESTION); + if (defaultQuestion.length() == 0) { + questionPref.setSummary(getResources().getString(R.string.pref_none)); + } + else { + questionPref.setSummary(defaultQuestion); + } + } +} diff --git a/app/src/main/java/com/wyattjmiller/studyhelper/StudyDatabase.java b/app/src/main/java/com/wyattjmiller/studyhelper/StudyDatabase.java new file mode 100644 index 0000000..145ae7b --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/studyhelper/StudyDatabase.java @@ -0,0 +1,229 @@ +package com.wyattjmiller.studyhelper; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.os.Build; +import java.util.ArrayList; +import java.util.List; + +import android.content.Context; +import androidx.room.Database; +import androidx.room.Room; +import androidx.room.RoomDatabase; + +public class StudyDatabase extends SQLiteOpenHelper { + + private static final int VERSION = 1; + private static final String DATABASE_NAME = "study.db"; + + private static StudyDatabase mStudyDb; + + public enum SubjectSortOrder { ALPHABETIC, UPDATE_DESC, UPDATE_ASC }; + + public static StudyDatabase getInstance(Context context) { + if (mStudyDb == null) { + mStudyDb = new StudyDatabase(context); + } + return mStudyDb; + } + + private StudyDatabase(Context context) { + super(context, DATABASE_NAME, null, VERSION); + } + + private static final class SubjectTable { + private static final String TABLE = "subjects"; + private static final String COL_TEXT = "text"; + private static final String COL_UPDATE_TIME = "updated"; + } + + private static final class QuestionTable { + private static final String TABLE = "questions"; + private static final String COL_ID = "_id"; + private static final String COL_TEXT = "text"; + private static final String COL_ANSWER = "answer"; + private static final String COL_SUBJECT = "subject"; + } + + @Override + public void onCreate(SQLiteDatabase db) { + + // Create subjects table + db.execSQL("create table " + SubjectTable.TABLE + " (" + + SubjectTable.COL_TEXT + " primary key, " + + SubjectTable.COL_UPDATE_TIME + " int)"); + + // Create questions table with foreign key that cascade deletes + db.execSQL("create table " + QuestionTable.TABLE + " (" + + QuestionTable.COL_ID + " integer primary key autoincrement, " + + QuestionTable.COL_TEXT + ", " + + QuestionTable.COL_ANSWER + ", " + + QuestionTable.COL_SUBJECT + ", " + + "foreign key(" + QuestionTable.COL_SUBJECT + ") references " + + SubjectTable.TABLE + "(" + SubjectTable.COL_TEXT + ") on delete cascade)"); + + // Add some subjects + String[] subjects = { "History", "Math", "Computing" }; + for (String sub: subjects) { + Subject subject = new Subject(sub); + ContentValues values = new ContentValues(); + values.put(SubjectTable.COL_TEXT, subject.getText()); + values.put(SubjectTable.COL_UPDATE_TIME, subject.getUpdateTime()); + db.insert(SubjectTable.TABLE, null, values); + } + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + db.execSQL("drop table if exists " + SubjectTable.TABLE); + db.execSQL("drop table if exists " + QuestionTable.TABLE); + onCreate(db); + } + + @Override + public void onOpen(SQLiteDatabase db) { + super.onOpen(db); + if (!db.isReadOnly()) { + // Enable foreign key constraints + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + db.execSQL("pragma foreign_keys = on;"); + } else { + db.setForeignKeyConstraintsEnabled(true); + } + } + } + + public List getSubjects(SubjectSortOrder order) { + List subjects = new ArrayList<>(); + + SQLiteDatabase db = this.getReadableDatabase(); + + String orderBy; + switch (order) { + case ALPHABETIC: + orderBy = SubjectTable.COL_TEXT + " collate nocase"; + break; + case UPDATE_DESC: + orderBy = SubjectTable.COL_UPDATE_TIME + " desc"; + break; + default: + orderBy = SubjectTable.COL_UPDATE_TIME + " asc"; + break; + } + + String sql = "select * from " + SubjectTable.TABLE + " order by " + orderBy; + Cursor cursor = db.rawQuery(sql, null); + if (cursor.moveToFirst()) { + do { + Subject subject = new Subject(); + subject.setText(cursor.getString(0)); + subject.setUpdateTime(cursor.getLong(1)); + subjects.add(subject); + } while (cursor.moveToNext()); + } + cursor.close(); + + return subjects; + } + + public boolean addSubject(Subject subject) { + SQLiteDatabase db = getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(SubjectTable.COL_TEXT, subject.getText()); + values.put(SubjectTable.COL_UPDATE_TIME, subject.getUpdateTime()); + long id = db.insert(SubjectTable.TABLE, null, values); + return id != -1; + } + + public void updateSubject(Subject subject) { + SQLiteDatabase db = getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(SubjectTable.COL_TEXT, subject.getText()); + values.put(SubjectTable.COL_UPDATE_TIME, subject.getUpdateTime()); + db.update(SubjectTable.TABLE, values, + SubjectTable.COL_TEXT + " = ?", new String[] { subject.getText() }); + } + + public void deleteSubject(Subject subject) { + SQLiteDatabase db = getWritableDatabase(); + db.delete(SubjectTable.TABLE, + SubjectTable.COL_TEXT + " = ?", new String[] { subject.getText() }); + } + + public List getQuestions(String subject) { + List questions = new ArrayList<>(); + + SQLiteDatabase db = this.getReadableDatabase(); + String sql = "select * from " + QuestionTable.TABLE + + " where " + QuestionTable.COL_SUBJECT + " = ?"; + Cursor cursor = db.rawQuery(sql, new String[] { subject }); + if (cursor.moveToFirst()) { + do { + Question question = new Question(); + question.setId(cursor.getInt(0)); + question.setText(cursor.getString(1)); + question.setAnswer(cursor.getString(2)); + question.setSubject(cursor.getString(3)); + questions.add(question); + } while (cursor.moveToNext()); + } + cursor.close(); + + return questions; + } + + public Question getQuestion(long questionId) { + Question question = null; + + SQLiteDatabase db = this.getReadableDatabase(); + String sql = "select * from " + QuestionTable.TABLE + + " where " + QuestionTable.COL_ID + " = ?"; + Cursor cursor = db.rawQuery(sql, new String[] { Float.toString(questionId) }); + + if (cursor.moveToFirst()) { + question = new Question(); + question.setId(cursor.getInt(0)); + question.setText(cursor.getString(1)); + question.setAnswer(cursor.getString(2)); + question.setSubject(cursor.getString(3)); + } + + return question; + } + + public void addQuestion(Question question) { + SQLiteDatabase db = getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(QuestionTable.COL_TEXT, question.getText()); + values.put(QuestionTable.COL_ANSWER, question.getAnswer()); + values.put(QuestionTable.COL_SUBJECT, question.getSubject()); + long questionId = db.insert(QuestionTable.TABLE, null, values); + question.setId(questionId); + + // Change update time in subjects table + updateSubject(new Subject(question.getSubject())); + } + + public void updateQuestion(Question question) { + SQLiteDatabase db = getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(QuestionTable.COL_ID, question.getId()); + values.put(QuestionTable.COL_TEXT, question.getText()); + values.put(QuestionTable.COL_ANSWER, question.getAnswer()); + values.put(QuestionTable.COL_SUBJECT, question.getSubject()); + db.update(QuestionTable.TABLE, values, + QuestionTable.COL_ID + " = " + question.getId(), null); + + // Change update time in subjects table + updateSubject(new Subject(question.getSubject())); + } + + public void deleteQuestion(long questionId) { + SQLiteDatabase db = getWritableDatabase(); + db.delete(QuestionTable.TABLE, + QuestionTable.COL_ID + " = " + questionId, null); + } +} diff --git a/app/src/main/java/com/wyattjmiller/studyhelper/StudyFetcher.java b/app/src/main/java/com/wyattjmiller/studyhelper/StudyFetcher.java new file mode 100644 index 0000000..0a22f69 --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/studyhelper/StudyFetcher.java @@ -0,0 +1,138 @@ +package com.wyattjmiller.studyhelper; + + + +import android.content.Context; +import android.net.Uri; +import android.util.Log; +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonObjectRequest; +import com.android.volley.toolbox.Volley; +import org.json.JSONArray; +import org.json.JSONObject; +import java.util.ArrayList; +import java.util.List; + +public class StudyFetcher { + + public interface OnStudyDataReceivedListener { + void onSubjectsReceived(List subjects); + void onQuestionsReceived(List questions); + void onErrorResponse(VolleyError error); + } + + private final String WEBAPI_BASE_URL = "https://wp.zybooks.com/study-helper.php"; + private final String TAG = "StudyFetcher"; + + private RequestQueue mRequestQueue; + + + public StudyFetcher(Context context) { + mRequestQueue = Volley.newRequestQueue(context); + } + + public void fetchSubjects(final OnStudyDataReceivedListener listener) { + + String url = Uri.parse(WEBAPI_BASE_URL).buildUpon() + .appendQueryParameter("type", "subjects").build().toString(); + + // Request all subjects + JsonObjectRequest request = new JsonObjectRequest + (Request.Method.GET, url, null, new Response.Listener() { + + @Override + public void onResponse(JSONObject response) { + List subjects = parseJsonSubjects(response); + listener.onSubjectsReceived(subjects); + } + }, new Response.ErrorListener() { + + @Override + public void onErrorResponse(VolleyError error) { + listener.onErrorResponse(error); + } + }); + + mRequestQueue.add(request); + } + + private List parseJsonSubjects(JSONObject json) { + + List subjects = new ArrayList(); + + // Create a list of subjects + try { + JSONArray subjectArray = json.getJSONArray("subjects"); + + for (int i = 0; i < subjectArray.length(); i++) { + JSONObject subjectObj = subjectArray.getJSONObject(i); + + Subject subject = new Subject(); + subject.setText(subjectObj.getString("subject")); + subject.setUpdateTime(subjectObj.getLong("updatetime")); + subjects.add(subject); + } + } + catch(Exception e){ + Log.e(TAG, "One or more fields not found in the JSON data"); + } + + return subjects; + } + + public void fetchQuestions(Subject subject, final OnStudyDataReceivedListener listener) { + + String url = Uri.parse(WEBAPI_BASE_URL).buildUpon() + .appendQueryParameter("type", "questions") + .appendQueryParameter("subject", subject.getText()) + .build().toString(); + + // Request questions for this subject + JsonObjectRequest jsObjRequest = new JsonObjectRequest + (Request.Method.GET, url, null, new Response.Listener() { + + @Override + public void onResponse(JSONObject response) { + List questions = parseJsonQuestions(response); + listener.onQuestionsReceived(questions); + } + }, new Response.ErrorListener() { + + @Override + public void onErrorResponse(VolleyError error) { + listener.onErrorResponse(error); + } + }); + + mRequestQueue.add(jsObjRequest); + } + + private List parseJsonQuestions(JSONObject json) { + + List questions = new ArrayList(); + + // Create a list of questions + try { + String subject = json.getString("subject"); + JSONArray questionArray = json.getJSONArray("questions"); + + for (int i = 0; i < questionArray.length(); i++) { + JSONObject questionObj = questionArray.getJSONObject(i); + + Question question = new Question(); + question.setText(questionObj.getString("question")); + question.setAnswer(questionObj.getString("answer")); + question.setSubject(subject); + questions.add(question); + } + } + catch(Exception e) { + Log.e(TAG, "One or more fields not found in the JSON data"); + } + + return questions; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/studyhelper/Subject.kt b/app/src/main/java/com/wyattjmiller/studyhelper/Subject.kt new file mode 100644 index 0000000..79ca5ed --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/studyhelper/Subject.kt @@ -0,0 +1,24 @@ +package com.wyattjmiller.studyhelper + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "subjects") +class Subject { + @PrimaryKey + @ColumnInfo(name = "text") + var text = "" + + @ColumnInfo(name = "updated") + var updateTime: Long + + constructor() { + updateTime = System.currentTimeMillis() + } + + constructor(text: String) { + this.text = text + updateTime = System.currentTimeMillis() + } +} diff --git a/app/src/main/java/com/wyattjmiller/studyhelper/SubjectActivity.kt b/app/src/main/java/com/wyattjmiller/studyhelper/SubjectActivity.kt new file mode 100644 index 0000000..28c285e --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/studyhelper/SubjectActivity.kt @@ -0,0 +1,254 @@ +package com.wyattjmiller.studyhelper + +import android.content.Intent +import android.content.SharedPreferences +import android.graphics.Color +import android.os.Bundle +import android.preference.PreferenceManager +import android.view.* +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView + +class SubjectActivity : AppCompatActivity(), SubjectDialogFragment.OnSubjectEnteredListener { + private lateinit var mStudyDb: StudyDatabase + private lateinit var mSubjectAdapter: SubjectAdapter + private lateinit var mRecyclerView: RecyclerView + private lateinit var mSubjectColors: IntArray + private lateinit var mSelectedSubject: Subject + private lateinit var mActionMode: ActionMode + private lateinit var mSharedPrefs: SharedPreferences + + private var mDarkTheme = false + private var mSelectedSubjectPosition = RecyclerView.NO_POSITION + + 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_subject) + mSubjectColors = resources.getIntArray(R.array.subjectColors) + + // Singleton + mStudyDb = StudyDatabase.getInstance(applicationContext) + mRecyclerView = findViewById(R.id.subjectRecyclerView) + + // Create 2 grid layout columns + val gridLayoutManager: RecyclerView.LayoutManager = GridLayoutManager(applicationContext, 2) + mRecyclerView.setLayoutManager(gridLayoutManager) + + // Shows the available subjects + mSubjectAdapter = SubjectAdapter(loadSubjects()) + mRecyclerView.setAdapter(mSubjectAdapter) + } + + + override fun onResume() { + super.onResume() + + // If theme changed, recreate the activity so theme is applied + val darkTheme = mSharedPrefs!!.getBoolean(SettingsFragment.PREFERENCE_THEME, false) + if (darkTheme != mDarkTheme) { + recreate() + } + + // Load subjects here in case settings changed + mSubjectAdapter = SubjectAdapter(loadSubjects()) + mRecyclerView.adapter = mSubjectAdapter + } + + private fun loadSubjects(): MutableList { + val order = mSharedPrefs!!.getString(SettingsFragment.PREFERENCE_SUBJECT_ORDER, "1") + return when (order!!.toInt()) { + 0 -> mStudyDb.getSubjects(StudyDatabase.SubjectSortOrder.ALPHABETIC) + 1 -> mStudyDb.getSubjects(StudyDatabase.SubjectSortOrder.UPDATE_DESC) + else -> mStudyDb.getSubjects(StudyDatabase.SubjectSortOrder.UPDATE_ASC) + } + } + + 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@SubjectActivity, SettingsActivity::class.java) + startActivity(intent) + true + } + R.id.import_questions -> { + val intent = Intent(this@SubjectActivity, ImportActivity::class.java) + startActivity(intent) + true + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onSubjectEntered(subject: String) { + // Returns subject entered in the SubjectDialogFragment dialog + if (subject.isNotEmpty()) { + val sub = Subject(subject) + if (mStudyDb!!.addSubject(sub)) { + mSubjectAdapter.addSubject(sub) + Toast.makeText(this, "Added $subject", Toast.LENGTH_SHORT).show() + } else { + val message = resources.getString(R.string.subject_exists, subject) + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() + } + } + } + + fun addSubjectClick(view: View?) { + // Prompt user to type new subject + val manager = supportFragmentManager + val dialog = SubjectDialogFragment() + dialog.show(manager, "subjectDialog") + } + + private inner class SubjectHolder(inflater: LayoutInflater, parent: ViewGroup?) : RecyclerView.ViewHolder(inflater.inflate(R.layout.recycler_view_items, parent, false)), View.OnClickListener, View.OnLongClickListener { + private var mSubject: Subject? = null + private val mTextView: TextView + + fun bind(subject: Subject, position: Int) { + mSubject = subject + mTextView.text = subject.text + if (mSelectedSubjectPosition == position) { + // Make selected subject stand out + mTextView.setBackgroundColor(Color.RED) + } else { + // Make the background color dependent on the length of the subject string + val colorIndex = subject.text!!.length % mSubjectColors.size + mTextView.setBackgroundColor(mSubjectColors[colorIndex]) + } + } + override fun onClick(view: View) { + // Start QuestionActivity, indicating what subject was clicked + val intent = Intent(this@SubjectActivity, QuestionActivity::class.java) + intent.putExtra(QuestionActivity.EXTRA_SUBJECT, mSubject!!.text) + startActivity(intent) + } + + override fun onLongClick(view: View?): Boolean { + if (mActionMode != null) { + return false + } + + mSelectedSubject = mSubject!! + mSelectedSubjectPosition = adapterPosition + + // Re-bind the selected item + mSubjectAdapter.notifyItemChanged(mSelectedSubjectPosition) + + // Show the CAB + mActionMode = this@SubjectActivity.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.getItemId()) { + R.id.delete -> { + // Delete from the database and remove from the RecyclerView + mStudyDb.deleteSubject(mSelectedSubject) + mSubjectAdapter.removeSubject(mSelectedSubject) + + // Close the CAB + mode.finish() + true + } + else -> false + } + } + + override fun onDestroyActionMode(mode: ActionMode) { + mActionMode = null + + // CAB closing, need to deselect item if not deleted + mSubjectAdapter.notifyItemChanged(mSelectedSubjectPosition) + mSelectedSubjectPosition = RecyclerView.NO_POSITION + } + } + + init { + itemView.setOnClickListener(this) + itemView.setOnLongClickListener(this) + mTextView = itemView.findViewById(R.id.subjectTextView) + } + } + + private inner class SubjectAdapter(private val mSubjectList: MutableList) : RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SubjectHolder { + val layoutInflater = LayoutInflater.from(applicationContext) + return SubjectHolder(layoutInflater, parent) + } + + override fun onBindViewHolder(holder: SubjectHolder, position: Int) { + holder.bind(mSubjectList[position], position) + } + + override fun getItemCount(): Int { + return mSubjectList.size + } + + fun onSubjectEntered(subject: String) { + // Returns subject entered in the SubjectDialogFragment dialog + if (subject.isNotEmpty()) { + val sub = Subject(subject) + if (mStudyDb.addSubject(sub)) { + mSubjectAdapter.addSubject(sub) +// Toast.makeText(this, subject, Toast.LENGTH_SHORT).show() + Toast.makeText(this@SubjectActivity, subject, Toast.LENGTH_LONG).show() + } else { + val message = resources.getString(R.string.subject_exists, subject) + Toast.makeText(this@SubjectActivity, message, Toast.LENGTH_SHORT).show() + } + } + } + + fun removeSubject(subject: Subject?) { + // Find subject in the list + val index = mSubjectList.indexOf(subject) + if (index >= 0) { + // Remove the subject + mSubjectList.removeAt(index) + + // Notify adapter of subject removal + notifyItemRemoved(index) + } + } + + fun addSubject(subject: Subject?) { + // Add the new subject at the beginning of the list + mSubjectList.add(0, subject!!) + + // Notify the adapter that item was added to the beginning of the list + notifyItemInserted(0) + + // Scroll to the top + mRecyclerView.scrollToPosition(0) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/studyhelper/SubjectDao.kt b/app/src/main/java/com/wyattjmiller/studyhelper/SubjectDao.kt new file mode 100644 index 0000000..a76160f --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/studyhelper/SubjectDao.kt @@ -0,0 +1,24 @@ +package com.wyattjmiller.studyhelper + +import androidx.room.* + +@Dao +interface SubjectDao { + @get:Query("SELECT * FROM subjects ORDER BY text") + val subjects: List? + + @get:Query("SELECT * FROM subjects ORDER BY updated DESC") + val subjectsNewerFirst: List? + + @get:Query("SELECT * FROM subjects ORDER BY updated ASC") + val subjectsOlderFirst: List? + + @Insert(onConflict = OnConflictStrategy.FAIL) + fun insertSubject(subject: Subject?) + + @Update + fun updateSubject(subject: Subject?) + + @Delete + fun deleteSubject(subject: Subject?) +} \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/studyhelper/SubjectDialogFragment.java b/app/src/main/java/com/wyattjmiller/studyhelper/SubjectDialogFragment.java new file mode 100644 index 0000000..364c9f4 --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/studyhelper/SubjectDialogFragment.java @@ -0,0 +1,45 @@ +package com.wyattjmiller.studyhelper; + + +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.text.InputType; +import android.widget.EditText; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; + +public class SubjectDialogFragment extends DialogFragment { + // Host activity must implement + public interface OnSubjectEnteredListener { + void onSubjectEntered(String subject); + } + + private OnSubjectEnteredListener mListener; + + @Override + public AlertDialog onCreateDialog(Bundle savedInstanceState) { + + final EditText subjectEditText = new EditText(getActivity()); + subjectEditText.setInputType(InputType.TYPE_CLASS_TEXT); + subjectEditText.setMaxLines(1); + + return new AlertDialog.Builder(getActivity()) + .setTitle(R.string.subject) + .setView(subjectEditText) + .setPositiveButton(R.string.create, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + String subject = subjectEditText.getText().toString(); + mListener.onSubjectEntered(subject.trim()); + } + }) + .setNegativeButton(R.string.cancel, null) + .create(); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + mListener = (OnSubjectEnteredListener) context; + } +} 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..02a1fb1 --- /dev/null +++ b/app/src/main/res/drawable/add.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/check.xml b/app/src/main/res/drawable/check.xml new file mode 100644 index 0000000..dcd3224 --- /dev/null +++ b/app/src/main/res/drawable/check.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/delete.xml b/app/src/main/res/drawable/delete.xml new file mode 100644 index 0000000..298674a --- /dev/null +++ b/app/src/main/res/drawable/delete.xml @@ -0,0 +1,11 @@ + + + + + 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/import_questions.xml b/app/src/main/res/drawable/import_questions.xml new file mode 100644 index 0000000..5cf9f5d --- /dev/null +++ b/app/src/main/res/drawable/import_questions.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/next.xml b/app/src/main/res/drawable/next.xml new file mode 100644 index 0000000..17f143a --- /dev/null +++ b/app/src/main/res/drawable/next.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/previous.xml b/app/src/main/res/drawable/previous.xml new file mode 100644 index 0000000..be59262 --- /dev/null +++ b/app/src/main/res/drawable/previous.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file 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/layout/activity_import.xml b/app/src/main/res/layout/activity_import.xml new file mode 100644 index 0000000..9a8343d --- /dev/null +++ b/app/src/main/res/layout/activity_import.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + +