diff --git a/Project 3 - Cody Cook (Weight Tracking App).zip b/Project 3 - Cody Cook (Weight Tracking App).zip
deleted file mode 100644
index cedec3b..0000000
Binary files a/Project 3 - Cody Cook (Weight Tracking App).zip and /dev/null differ
diff --git a/Project/.gitignore b/Project/.gitignore
new file mode 100644
index 0000000..aa724b7
--- /dev/null
+++ b/Project/.gitignore
@@ -0,0 +1,15 @@
+*.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
+local.properties
diff --git a/Project/app/.gitignore b/Project/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/Project/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/Project/app/build.gradle.kts b/Project/app/build.gradle.kts
new file mode 100644
index 0000000..85ecc26
--- /dev/null
+++ b/Project/app/build.gradle.kts
@@ -0,0 +1,40 @@
+plugins {
+ alias(libs.plugins.androidApplication)
+}
+
+android {
+ namespace = "edu.snhu.cs360.ccook.project"
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "edu.snhu.cs360.ccook.project"
+ minSdk = 34
+ targetSdk = 34
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+
+ implementation(libs.appcompat)
+ implementation(libs.material)
+ implementation(libs.activity)
+ implementation(libs.constraintlayout)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.ext.junit)
+ androidTestImplementation(libs.espresso.core)
+}
\ No newline at end of file
diff --git a/Project/app/proguard-rules.pro b/Project/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/Project/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/Project/app/src/androidTest/java/edu/snhu/cs360/ccook/project/ExampleInstrumentedTest.java b/Project/app/src/androidTest/java/edu/snhu/cs360/ccook/project/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..b8605bb
--- /dev/null
+++ b/Project/app/src/androidTest/java/edu/snhu/cs360/ccook/project/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package edu.snhu.cs360.ccook.project;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("edu.snhu.cs360.ccook.project", appContext.getPackageName());
+ }
+}
\ No newline at end of file
diff --git a/Project/app/src/main/AndroidManifest.xml b/Project/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c5df94a
--- /dev/null
+++ b/Project/app/src/main/AndroidManifest.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Project/app/src/main/java/edu/snhu/cs360/ccook/project/Database.java b/Project/app/src/main/java/edu/snhu/cs360/ccook/project/Database.java
new file mode 100644
index 0000000..5abbe66
--- /dev/null
+++ b/Project/app/src/main/java/edu/snhu/cs360/ccook/project/Database.java
@@ -0,0 +1,161 @@
+package edu.snhu.cs360.ccook.project;
+
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+
+import androidx.fragment.app.FragmentActivity;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+public class Database extends SQLiteOpenHelper {
+
+ // define database file and version
+ private static final String DATABASE_NAME = String.valueOf(R.string.database_file);
+ private static final int VERSION = 17;
+
+ // set constructors
+ public Database(Login context) {
+ super(context, DATABASE_NAME, null, VERSION);
+ }
+
+ public Database(Main context) {
+ super(context, DATABASE_NAME, null, VERSION);
+ }
+
+ public Database(Home context) {
+ super(context, DATABASE_NAME, null, VERSION);
+ }
+
+ public Database(FragmentActivity activity) {
+ super(activity, DATABASE_NAME, null, VERSION);
+ }
+
+
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ // create a new database if it doesn't exist
+ Log.d(String.valueOf(R.string.database), String.valueOf(R.string.creating_new_database_with_tables));
+ db.execSQL("CREATE TABLE " + UserTable.TABLE + " (" + UserTable.COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + UserTable.COL_USERNAME + " TEXT, " + UserTable.COL_PASSWORD + " TEXT, " + UserTable.COL_GOAL + " REAL, " + UserTable.COL_GOAL_UNIT + " TEXT)");
+ db.execSQL("CREATE TABLE " + WeightTable.TABLE + " (" + WeightTable.COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + WeightTable.COL_USER_ID + " INTEGER, " + WeightTable.COL_DATE + " TEXT, " + WeightTable.COL_WEIGHT + " REAL, " + WeightTable.COL_WEIGHT_UNIT + " TEXT)");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ // purge existing database and create new
+ Log.d(String.valueOf(R.string.database), String.valueOf(R.string.upgrading_database_due_to_version_change_wiping_data));
+ db.execSQL("DROP TABLE IF EXISTS " + UserTable.TABLE);
+ db.execSQL("DROP TABLE IF EXISTS " + WeightTable.TABLE);
+ onCreate(db);
+ }
+
+ private String encryptPassword(String password) throws NoSuchAlgorithmException {
+ // store password in database as a hash for security
+ MessageDigest hash = MessageDigest.getInstance("SHA-256");
+ return Arrays.toString(hash.digest(password.getBytes(StandardCharsets.UTF_8)));
+ }
+
+ public Cursor checkUserExists(SQLiteDatabase db, String username) {
+ // look if the user exists
+ Log.d(String.valueOf(R.string.database), R.string.checking_database_for_user + username);
+ return db.rawQuery("SELECT " + UserTable.COL_ID + " FROM " + UserTable.TABLE + " WHERE " + UserTable.COL_USERNAME + " = ?", new String[]{username});
+ }
+
+ public Cursor checkUserPassword(SQLiteDatabase db, String username, String password) throws NoSuchAlgorithmException {
+ // return the id if the user exists with a matching password
+ Log.d(String.valueOf(R.string.database), R.string.validating_password_for_user + username);
+ return db.rawQuery("SELECT " + UserTable.COL_ID + " FROM " + UserTable.TABLE + " WHERE " + UserTable.COL_USERNAME + " = ? AND " + UserTable.COL_PASSWORD + " = ?", new String[]{username, encryptPassword(password)});
+ }
+
+ public void registerUser(SQLiteDatabase db, String username, String password) throws NoSuchAlgorithmException {
+ // Prepare default values for the new user
+ double defaultGoalWeight = -1; // Default goal weight
+ String defaultGoalUnit = String.valueOf(R.string.lb); // Default goal weight unit
+
+ // Log registration attempt
+ Log.d(String.valueOf(R.string.database), String.valueOf(R.string.registering_new_user) + username);
+
+ // Execute SQL command to insert new user with default goal values
+ String sql = "INSERT INTO " + UserTable.TABLE + " (" + UserTable.COL_USERNAME + ", " + UserTable.COL_PASSWORD + ", " + UserTable.COL_GOAL + ", " + UserTable.COL_GOAL_UNIT + ") VALUES (?, ?, ?, ?)";
+
+ db.execSQL(sql, new String[]{username, encryptPassword(password), String.valueOf(defaultGoalWeight), defaultGoalUnit});
+ }
+
+ public void addWeight(SQLiteDatabase db, int user_id, String date, double weight, String weight_unit) {
+ // add weight to the database
+ Log.d(String.valueOf(R.string.database), String.valueOf(R.string.adding_weight_for_user_id) + user_id);
+ db.execSQL("INSERT INTO " + WeightTable.TABLE + " (" + WeightTable.COL_USER_ID + ", " + WeightTable.COL_DATE + ", " + WeightTable.COL_WEIGHT + ", " + WeightTable.COL_WEIGHT_UNIT + ") VALUES (?, ?, ?, ?)", new String[]{String.valueOf(user_id), date, String.valueOf(weight), weight_unit});
+ }
+
+ public Cursor getUserWeights(SQLiteDatabase db, int userId, boolean sortDesc) {
+ // get a user's weight based on their userId, sort as needed
+ Log.d(String.valueOf(R.string.database), String.valueOf(R.string.grabbing_weights_in_for_user_id) + userId);
+ String sort = "DESC";
+ if (!sortDesc) {
+ sort = "ASC";
+ }
+ return db.rawQuery("SELECT * FROM " + WeightTable.TABLE + " WHERE " + WeightTable.COL_USER_ID + " = ? ORDER BY " + WeightTable.COL_DATE + " " + sort, new String[]{String.valueOf(userId)});
+ }
+
+ public Cursor getWeight(SQLiteDatabase db, int userId, String date) {
+ // get weight; not currently used
+ return db.rawQuery("SELECT * FROM " + WeightTable.TABLE + " WHERE " + WeightTable.COL_USER_ID + " = ? AND " + WeightTable.COL_DATE + " = ?", new String[]{String.valueOf(userId), date});
+ }
+
+ public Cursor getRecentWeight(SQLiteDatabase db, int user_id) {
+ // pull the most recent weight of the user
+ return db.rawQuery("SELECT " + WeightTable.COL_WEIGHT + "," + WeightTable.COL_WEIGHT_UNIT + " FROM " + WeightTable.TABLE + " WHERE " + WeightTable.COL_USER_ID + " = ? ORDER BY " + WeightTable.COL_DATE + " DESC LIMIT 1", new String[]{String.valueOf(user_id)});
+ }
+
+ public void removeWeight(SQLiteDatabase db, int id) {
+ // remove the specific weight from the table
+ db.execSQL("DELETE FROM " + WeightTable.TABLE + " WHERE " + WeightTable.COL_ID + " = ?", new String[]{String.valueOf(id)});
+ }
+
+ public void setWeightGoal(SQLiteDatabase db, int userId, double goalWeight, String goalWeightUnit) {
+ // set the weight goal to the user
+ db.execSQL("UPDATE " + UserTable.TABLE + " SET " + UserTable.COL_GOAL + " = ?, " + UserTable.COL_GOAL_UNIT + " = ? WHERE " + UserTable.COL_ID + " = ?", new String[]{String.valueOf(goalWeight), goalWeightUnit, String.valueOf(userId)});
+ }
+
+ public void clearWeightGoal(SQLiteDatabase db, int userId) {
+ // remove the weight goal
+ db.execSQL("UPDATE " + UserTable.TABLE + " SET " + UserTable.COL_GOAL + " = ?, " + UserTable.COL_GOAL_UNIT + " = ? WHERE " + UserTable.COL_ID + " = ?", new String[]{"-1", String.valueOf(R.string.lb), String.valueOf(userId)});
+ }
+
+ public Cursor getWeightGoal(SQLiteDatabase db, int userId) {
+ // get user's goal weight
+ return db.rawQuery("SELECT " + UserTable.COL_GOAL + "," + UserTable.COL_GOAL_UNIT + " FROM " + UserTable.TABLE + " WHERE " + UserTable.COL_ID + " = ?", new String[]{String.valueOf(userId)});
+ }
+
+ public void updateWeight(SQLiteDatabase db, int id, double weight, String weightUnit) {
+ // update the weight
+ db.execSQL("UPDATE " + WeightTable.TABLE + " SET " + WeightTable.COL_WEIGHT + " = ?, " + WeightTable.COL_WEIGHT_UNIT + " = ? WHERE " + WeightTable.COL_ID + " = ?", new String[]{String.valueOf(weight), weightUnit, String.valueOf(id)});
+}
+
+ /* Tables */
+ public static final class UserTable {
+
+ public static final String TABLE = "users";
+ public static final String COL_ID = "_id";
+ public static final String COL_USERNAME = "username";
+ public static final String COL_PASSWORD = "password";
+ public static final String COL_GOAL = "goal_weight";
+ public static final String COL_GOAL_UNIT = "goal_weight_unit";
+ }
+
+ public static final class WeightTable {
+ public static final String TABLE = "weight";
+ public static final String COL_ID = "_id";
+ public static final String COL_USER_ID = "user_id";
+ public static final String COL_DATE = "date";
+ public static final String COL_WEIGHT = "weight";
+ public static final String COL_WEIGHT_UNIT = "weight_unit";
+ }
+
+
+}
diff --git a/Project/app/src/main/java/edu/snhu/cs360/ccook/project/Home.java b/Project/app/src/main/java/edu/snhu/cs360/ccook/project/Home.java
new file mode 100644
index 0000000..c3adb15
--- /dev/null
+++ b/Project/app/src/main/java/edu/snhu/cs360/ccook/project/Home.java
@@ -0,0 +1,309 @@
+package edu.snhu.cs360.ccook.project;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Bundle;
+import android.telephony.SmsManager;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.PopupMenu;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.activity.EdgeToEdge;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.ContextCompat;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+
+public class Home extends AppCompatActivity implements WeightEntryAdapter.OnEntryInteractionListener {
+ boolean sortDesc = true;
+ private Database dbHelper;
+ private RecyclerView recyclerView;
+ private Button goalWeightButton, currentWeightButton, weightDifferenceButton;
+ private TextView losegainTextView;
+ private double currentWeight;
+ private double goalWeight;
+ private boolean sentTextMessageThisSession = false;
+
+
+ // convert lb to kg if necessary; future feature
+ public double lbsToKg(double lbs) {
+ return lbs * 0.453592;
+ }
+
+ // convert kg to lb
+ public double kgToLbs(double kg) {
+ return kg * 2.20462;
+ }
+
+ void fetchWeightEntries(int userId, boolean sortDesc) {
+ // build the list of weights with a new thread
+ new Thread(() -> {
+ List weightEntries = new ArrayList<>();
+ SQLiteDatabase db = dbHelper.getReadableDatabase();
+ Cursor cursor = dbHelper.getUserWeights(db, userId, sortDesc);
+ try {
+ if (cursor.moveToFirst()) {
+ do {
+ // get column details
+ @SuppressLint("Range") int id = cursor.getInt(cursor.getColumnIndex(Database.WeightTable.COL_ID));
+ @SuppressLint("Range") String date = cursor.getString(cursor.getColumnIndex(Database.WeightTable.COL_DATE));
+ @SuppressLint("Range") double weight = cursor.getDouble(cursor.getColumnIndex(Database.WeightTable.COL_WEIGHT));
+ @SuppressLint("Range") String unit = cursor.getString(cursor.getColumnIndex(Database.WeightTable.COL_WEIGHT_UNIT));
+
+ // date is an epoch; convert to Mm/dd/yy
+ long epochMillis = Long.parseLong(date) * 1000; // Convert seconds to milliseconds
+ date = new SimpleDateFormat(getString(R.string.dateFormat)).format(new Date(epochMillis));
+
+ if (unit.equalsIgnoreCase(getString(R.string.kg))) {
+ weight = kgToLbs(weight);
+ // round weight to the closest
+ weight = (double) Math.round(weight);
+ unit = getString(R.string.lb);
+ }
+ weightEntries.add(new WeightEntry(id, userId, date, weight, unit));
+ } while (cursor.moveToNext());
+ } else {
+ this.currentWeight = 0.0;
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ cursor.close();
+
+ // Update the RecyclerView on the main thread
+ runOnUiThread(() -> recyclerView.setAdapter(new WeightEntryAdapter(weightEntries, this)));
+ }).start();
+ }
+
+ private void goalAchieved() {
+ // alert the user on goal achievement
+ if (!sentTextMessageThisSession) {
+ // only alert on the first time you open the app per session
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS) == PackageManager.PERMISSION_GRANTED) {
+
+ // the Android Device manager phone number was +15551234567; reusing here without requesting new permissions
+ String phoneNo = getString(R.string.phoneNumber);
+ String message = getString(R.string.congrats_on_reaching_your_goal);
+
+ try {
+ SmsManager smsManager = SmsManager.getDefault();
+ smsManager.sendTextMessage(phoneNo, null, message, null, null);
+ } catch (Exception e) {
+ Toast.makeText(getApplicationContext(), String.valueOf(R.string.sms_failed_to_send_please_try_again), Toast.LENGTH_LONG).show();
+ e.printStackTrace();
+ }
+ } else {
+ Toast.makeText(this, String.valueOf(R.string.congrats_on_reaching_your_goal_we_know_you_could_do_it), Toast.LENGTH_LONG).show();
+ }
+ }
+ sentTextMessageThisSession = true;
+ }
+
+
+ @SuppressLint("Range")
+ private void setGoalWeightText(int userId) {
+ String string;
+ double weight;
+ String unit;
+
+ SQLiteDatabase db = dbHelper.getReadableDatabase();
+ Cursor cursor = dbHelper.getWeightGoal(db, userId);
+
+ if (cursor.moveToFirst()) {
+ weight = cursor.getDouble(cursor.getColumnIndex(Database.UserTable.COL_GOAL));
+ unit = cursor.getString(cursor.getColumnIndex(Database.UserTable.COL_GOAL_UNIT));
+ if (unit.equalsIgnoreCase(getString(R.string.kg))) {
+ weight = kgToLbs(weight);
+ weight = Math.round(weight);
+ unit = getString(R.string.lb);
+ }
+
+ if (weight <= 0) {
+ string = getString(R.string.unset);
+ } else {
+ string = weight + " " + unit;
+ }
+ this.goalWeight = weight;
+ goalWeightButton.setText(string);
+ }
+ }
+
+ private void setLoseGainText() {
+ double difference = this.goalWeight - this.currentWeight;
+ String string = Math.abs(difference) + " " + getString(R.string.lb);
+
+ // if the goalWeight and currentWeight are set
+ if (this.goalWeight > 0.0 && this.currentWeight > 0.0) {
+ // and there is no difference
+ if (difference == 0.0) {
+ // the goal was met
+ losegainTextView.setText(getString(R.string.goal_met));
+ string = getString(R.string.congrats);
+ goalAchieved();
+ }
+ // if the difference is negative
+ if (difference < 0.0) {
+ losegainTextView.setText(getString(R.string.to_lose));
+ } else if (difference > 0.0) {
+ losegainTextView.setText(getString(R.string.to_gain));
+ }
+ } else {
+ losegainTextView.setText(getString(R.string.lose_gain));
+ string = "";
+ }
+ weightDifferenceButton.setText(string);
+ }
+
+ @SuppressLint("Range")
+ private void setCurrentWeight(int userId) {
+ double weight = 0.0;
+ String unit;
+ String string = getString(R.string.unset);
+
+ SQLiteDatabase db = dbHelper.getReadableDatabase();
+ Cursor cursor = dbHelper.getRecentWeight(db, userId);
+
+ if (cursor.moveToFirst()) {
+ weight = cursor.getDouble(cursor.getColumnIndex(Database.WeightTable.COL_WEIGHT));
+ unit = cursor.getString(cursor.getColumnIndex(Database.WeightTable.COL_WEIGHT_UNIT));
+ if (unit.equalsIgnoreCase(getString(R.string.kg))) {
+ weight = kgToLbs(weight);
+ // round weight to the closest
+ weight = Math.round(weight);
+ unit = getString(R.string.lb);
+ }
+ if (weight > 0) {
+ string = weight + " " + unit;
+ }
+ }
+ this.currentWeight = weight;
+ currentWeightButton.setText(string);
+ }
+
+ void setBannerWidgets(int userId) {
+ // update the banner widgets
+ setCurrentWeight(userId);
+ setGoalWeightText(userId);
+ setLoseGainText();
+
+ }
+
+
+ private void toggleSortOrder() {
+ // toggle sorting
+ sortDesc = !sortDesc;
+
+ ImageView sortByDateIcon = findViewById(R.id.sortByDateIcon);
+
+ if (sortDesc) {
+ sortByDateIcon.setImageResource(R.drawable.sortdesc);
+ } else {
+ sortByDateIcon.setImageResource(R.drawable.sortasc);
+ }
+
+ // retrieve the entries in the requested order and update banner widgets, if needed
+ fetchWeightEntries(getIntent().getIntExtra("userId", -1), sortDesc);
+ setBannerWidgets(getIntent().getIntExtra("userId", -1));
+ }
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ EdgeToEdge.enable(this);
+ setContentView(R.layout.home);
+
+ dbHelper = new Database(this);
+ recyclerView = findViewById(R.id.recyclerView);
+ recyclerView.setLayoutManager(new LinearLayoutManager(this));
+ goalWeightButton = findViewById(R.id.goalWeightButton);
+ currentWeightButton = findViewById(R.id.currentWeightButton);
+ weightDifferenceButton = findViewById(R.id.weightDifferenceButton);
+ losegainTextView = findViewById(R.id.losegainTextView);
+ FloatingActionButton addButton = findViewById(R.id.addDataButton);
+ ImageView sortByDateIcon = findViewById(R.id.sortByDateIcon);
+
+ // set a listener for the sort toggler
+ sortByDateIcon.setOnClickListener(v -> toggleSortOrder());
+
+ // pull in the userId from the previous page
+ Intent getExtras = getIntent();
+ int userId = getExtras.getIntExtra("userId", -1);
+
+ // get the latest weight entries with the proper sort
+ fetchWeightEntries(userId, sortDesc);
+
+ // add button tap listener; bring up menu
+ addButton.setOnClickListener(v -> {
+ PopupMenu popup = new PopupMenu(this, v);
+ popup.getMenuInflater().inflate(R.menu.add, popup.getMenu());
+ popup.setOnMenuItemClickListener(item -> {
+ int id = item.getItemId();
+ if (id == R.id.record_weight) {
+ // Record weight here
+ RecordWeight dialogFragment = RecordWeight.newInstance(userId, -1, RecordWeight.getDate(),-1, false);
+ dialogFragment.show(getSupportFragmentManager(), "RecordWeight");
+ fetchWeightEntries(getIntent().getIntExtra("userId", -1), sortDesc);
+ return true;
+ } else if (id == R.id.set_Goal) {
+ // Set the goal weight here
+ SetGoal dialogFragment = SetGoal.newInstance(userId);
+ dialogFragment.show(getSupportFragmentManager(), "SetGoal");
+ return true;
+ }
+ return false;
+ });
+ popup.show();
+ });
+
+ // now that data is loaded, set banners.
+ setBannerWidgets(userId);
+
+ ViewCompat.setOnApplyWindowInsetsListener(recyclerView, (v, insets) -> {
+ v.setPadding(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom());
+ return WindowInsetsCompat.CONSUMED;
+ });
+
+
+ }
+
+ @Override
+ public void onEditClicked(WeightEntry entry) {
+ // Show dialog for editing
+ RecordWeight dialogFragment = RecordWeight.newInstance(entry.getUserId(), entry.getId(), entry.getDate(), entry.getWeight(), Objects.equals(entry.getWeightUnit(), String.valueOf(R.string.kg)));
+ dialogFragment.show(getSupportFragmentManager(), "EditWeight");
+
+ // Refresh the list
+ fetchWeightEntries(entry.getUserId(), sortDesc);
+ setBannerWidgets(entry.getUserId());
+ }
+
+ @Override
+ public void onDeleteClicked(WeightEntry entry) {
+ SQLiteDatabase db = dbHelper.getWritableDatabase();
+ dbHelper.removeWeight(db, entry.getId());
+ db.close();
+
+ // Refresh the list
+ fetchWeightEntries(entry.getUserId(), sortDesc);
+ setBannerWidgets(entry.getUserId());
+ }
+
+}
diff --git a/Project/app/src/main/java/edu/snhu/cs360/ccook/project/Login.java b/Project/app/src/main/java/edu/snhu/cs360/ccook/project/Login.java
new file mode 100644
index 0000000..59c7b54
--- /dev/null
+++ b/Project/app/src/main/java/edu/snhu/cs360/ccook/project/Login.java
@@ -0,0 +1,169 @@
+package edu.snhu.cs360.ccook.project;
+
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import androidx.activity.EdgeToEdge;
+import androidx.appcompat.app.AppCompatActivity;
+
+public class Login extends AppCompatActivity {
+
+ private EditText textUsername, textPassword;
+ private Button buttonLogin, buttonRegister;
+ private Database dbHelper;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ EdgeToEdge.enable(this);
+ setContentView(R.layout.login);
+ dbHelper = new Database(this);
+
+ // identify the fields in the layout
+ textUsername = findViewById(R.id.textUsername);
+ textPassword = findViewById(R.id.textPassword);
+ buttonLogin = findViewById(R.id.buttonLogin);
+ buttonRegister = findViewById(R.id.buttonRegister);
+
+ // set watchers to watch changes in text
+ textUsername.addTextChangedListener(new UserTextWatcher());
+ textPassword.addTextChangedListener(new PasswordTextWatcher());
+
+ // set button listeners
+ buttonLogin.setOnClickListener(v -> loginUser());
+ buttonRegister.setOnClickListener(v -> registerUser());
+
+ // Initially disable both buttons
+ updateButtonStates();
+ }
+
+ private void updateButtonStates() {
+ String username = textUsername.getText().toString().trim();
+ boolean hasPassword = textPassword.getText().length() > 0;
+
+ // when the username isn't empty and there is a password
+ if (!username.isEmpty() && hasPassword) {
+ new Thread(() -> {
+ // find if this user exists
+ SQLiteDatabase db = dbHelper.getReadableDatabase();
+ Cursor cursor = dbHelper.checkUserExists(db, username);
+ final boolean userExists = cursor.getCount() > 0;
+ cursor.close();
+ db.close();
+
+ runOnUiThread(() -> {
+ // either register or login based on experience
+ buttonRegister.setEnabled(!userExists && hasPassword);
+ buttonLogin.setEnabled(userExists && hasPassword);
+ });
+ }).start();
+ } else {
+ runOnUiThread(() -> {
+ // default to disabled when the conditions are not met
+ buttonRegister.setEnabled(false);
+ buttonLogin.setEnabled(false);
+ });
+ }
+ }
+
+ private void registerUser() {
+ String username = textUsername.getText().toString().trim();
+ String password = textPassword.getText().toString().trim();
+
+ new Thread(() -> {
+ // Encrypt the password before storing it
+ try (SQLiteDatabase db = dbHelper.getWritableDatabase()) {
+ dbHelper.registerUser(db, username, password);
+ runOnUiThread(() -> {
+ Toast.makeText(Login.this, getString(R.string.registration_successful), Toast.LENGTH_SHORT).show();
+ textPassword.setText(""); // Clear password field after registration for security
+ buttonRegister.setEnabled(false); // Disable register button as this user now exists
+ });
+ } catch (Exception e) {
+ Log.e(getString(R.string.loginactivity), getString(R.string.failed_to_register_user), e);
+ runOnUiThread(() -> Toast.makeText(Login.this, String.valueOf(R.string.registration_failed), Toast.LENGTH_SHORT).show());
+ }
+ }).start();
+ }
+
+ private void loginUser() {
+ String username = textUsername.getText().toString().trim();
+ String password = textPassword.getText().toString().trim();
+
+ new Thread(() -> {
+ try (SQLiteDatabase db = dbHelper.getReadableDatabase()) {
+ Cursor cursor = dbHelper.checkUserPassword(db, username, password);
+
+ // Check login success
+ final boolean loginSuccess = cursor.moveToFirst();
+
+ // Get user ID if login successful
+ final int userId = loginSuccess ? cursor.getInt(0) : -1;
+
+ runOnUiThread(() -> {
+ if (loginSuccess) {
+ Toast.makeText(Login.this, getString(R.string.login_successful), Toast.LENGTH_SHORT).show();
+ Intent intent = new Intent(Login.this, Home.class);
+ intent.putExtra("userId", userId);
+
+ // start the activity
+ startActivity(intent);
+
+ // Close login activity
+ finish();
+ } else {
+ Toast.makeText(Login.this, getString(R.string.authentication_failed), Toast.LENGTH_SHORT).show();
+ // Clear password field on failed login
+ textPassword.setText("");
+ }
+ });
+
+ cursor.close(); // Close cursor after data retrieval
+ } catch (Exception e) {
+ Log.e(getString(R.string.loginactivity), getString(R.string.failed_to_login_user), e);
+ runOnUiThread(() -> Toast.makeText(Login.this, getString(R.string.login_error), Toast.LENGTH_SHORT).show());
+ }
+ }).start();
+ }
+
+
+ private class UserTextWatcher implements TextWatcher {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ updateButtonStates();
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ }
+
+ private class PasswordTextWatcher implements TextWatcher {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ updateButtonStates();
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/Project/app/src/main/java/edu/snhu/cs360/ccook/project/Main.java b/Project/app/src/main/java/edu/snhu/cs360/ccook/project/Main.java
new file mode 100644
index 0000000..e80ef3b
--- /dev/null
+++ b/Project/app/src/main/java/edu/snhu/cs360/ccook/project/Main.java
@@ -0,0 +1,66 @@
+package edu.snhu.cs360.ccook.project;
+
+import android.Manifest;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+
+public class Main extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Initialize database (checks if it exists and creates if not)
+ initializeDatabase();
+
+ // Check SMS permissions and navigate accordingly
+ checkPermissions();
+ }
+
+ private void initializeDatabase() {
+ try {
+ // Create database helper
+ Database dbHelper = new Database(this);
+
+ // Calling getWritableDatabase() ensures database is created if it doesn't exist
+ dbHelper.getWritableDatabase();
+
+ } catch (Exception e) {
+ // User output for an error occurred with the db
+ Toast.makeText(Main.this, "Error initializing database", Toast.LENGTH_SHORT).show();
+ Log.e("Main", "Error initializing database", e);
+ }
+ }
+
+ private void checkPermissions() {
+ // Check if app previously given SMS permissions
+ if (ActivityCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS) == PackageManager.PERMISSION_GRANTED) {
+ // Permission already exists, skip requesting SMS permissions and go to login page
+ showLogin();
+ } else {
+ // Permission wasn't granted, request it
+ requestSMS();
+ }
+
+ // Finish Main activity
+ finish();
+ }
+
+ private void showLogin() {
+ // Show the login screen
+ Intent loginIntent = new Intent(this, Login.class);
+ startActivity(loginIntent);
+ }
+
+ private void requestSMS() {
+ // Show the SMS request permission screen
+ Intent smsIntent = new Intent(this, SMS.class);
+ startActivity(smsIntent);
+ }
+}
\ No newline at end of file
diff --git a/Project/app/src/main/java/edu/snhu/cs360/ccook/project/RecordWeight.java b/Project/app/src/main/java/edu/snhu/cs360/ccook/project/RecordWeight.java
new file mode 100644
index 0000000..84a7a12
--- /dev/null
+++ b/Project/app/src/main/java/edu/snhu/cs360/ccook/project/RecordWeight.java
@@ -0,0 +1,107 @@
+package edu.snhu.cs360.ccook.project;
+
+import android.app.Dialog;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.RadioGroup;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+
+public class RecordWeight extends DialogFragment {
+
+ private EditText weightInput;
+ private RadioGroup weightUnitGroup;
+ private Database dbHelper;
+
+
+ public static RecordWeight newInstance(int userId, int entryId, String date, double weight, boolean isKg) {
+ // configure a new fragment with the user id so it can be updated or added
+ RecordWeight fragment = new RecordWeight();
+ Bundle args = new Bundle();
+ args.putInt("userId", userId);
+ args.putInt("entryId", entryId);
+ args.putDouble("weight", weight);
+ args.putString("date", date);
+ args.putBoolean("isKg", isKg);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ public static String getDate() {
+ // parse timestamp properly
+ return String.valueOf(System.currentTimeMillis() / 1000);
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ // start a new database connection
+ dbHelper = new Database(getActivity());
+
+ // build view and layout for fragment dialog
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ LayoutInflater inflater = requireActivity().getLayoutInflater();
+ View view = inflater.inflate(R.layout.record_weight, null);
+
+ // identify the fields in the layout
+ weightInput = view.findViewById(R.id.weight_input);
+ weightUnitGroup = view.findViewById(R.id.weight_unit_group);
+
+ Bundle args = getArguments();
+ assert args != null;
+ int entryId = args.getInt("entryId", -1);
+
+ // Check if it's an edit
+ if (entryId != -1) {
+ double weight = args.getDouble("weight");
+ boolean isKg = args.getBoolean("isKg");
+ weightInput.setText(String.valueOf(weight));
+ weightUnitGroup.check(isKg ? R.id.unit_kg : R.id.unit_lb);
+ }
+
+ // pop open a Record Weight window
+ builder.setView(view).setMessage(entryId == -1 ? "Record Weight" : "Edit Weight").setPositiveButton("Save", (dialog, id) -> {
+ assert getArguments() != null;
+
+ // on save, confirm it looks okay
+ validateAndAddOrEditWeight(entryId);
+
+ // refresh the Home screen after adding
+ ((Home) getActivity()).fetchWeightEntries(getArguments().getInt("userId", -1), ((Home) getActivity()).sortDesc);
+ ((Home) getActivity()).setBannerWidgets(getArguments().getInt("userId", -1));
+
+ }).setNegativeButton("Cancel", (dialog, id) -> {
+ // cancel out if desired
+ dismiss();
+ });
+ // return dialog box
+ return builder.create();
+ }
+
+
+ private void validateAndAddOrEditWeight(int entryId) {
+ int userId = getArguments().getInt("userId", -1);
+ String weightStr = weightInput.getText().toString();
+ double weight = Double.parseDouble(weightStr);
+ SQLiteDatabase db = dbHelper.getWritableDatabase();
+ boolean isKg = weightUnitGroup.getCheckedRadioButtonId() == R.id.unit_kg;
+ String weightUnit = isKg ? getString(R.string.kg) : getString(R.string.lb);
+
+ if ((isKg && (weight < 20 || weight > 430)) || (!isKg && (weight < 45 || weight > 950))) {
+ Toast.makeText(getContext(), isKg ? "Weight must be between 20 kg and 430 kg" : "Weight must be between 45 lbs and 950 lbs", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (entryId == -1) {
+ dbHelper.addWeight(db, userId, getDate(), weight, weightUnit);
+ } else {
+ dbHelper.updateWeight(db, entryId, weight, weightUnit);
+ }
+ }
+}
diff --git a/Project/app/src/main/java/edu/snhu/cs360/ccook/project/SMS.java b/Project/app/src/main/java/edu/snhu/cs360/ccook/project/SMS.java
new file mode 100644
index 0000000..47334ac
--- /dev/null
+++ b/Project/app/src/main/java/edu/snhu/cs360/ccook/project/SMS.java
@@ -0,0 +1,66 @@
+package edu.snhu.cs360.ccook.project;
+
+import android.Manifest;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.widget.Button;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+
+public class SMS extends AppCompatActivity {
+
+ private static final int SMS_PERMISSION_CODE = 101;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.sms);
+
+ // set request SMS button ID
+ Button btnRequestSMSPermission = findViewById(R.id.btnRequestSMSPermission);
+
+ // set onclick listener
+ btnRequestSMSPermission.setOnClickListener(view -> {
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS) != PackageManager.PERMISSION_GRANTED) {
+ // request permissions
+ ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.SEND_SMS}, SMS_PERMISSION_CODE);
+ } else {
+ // Permission has already been granted
+ permissionGranted();
+ }
+ });
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ if (requestCode == SMS_PERMISSION_CODE) {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ // Permission granted
+ permissionGranted();
+ } else {
+ // Permission denied
+ permissionRejected();
+ }
+ }
+ }
+
+ private void permissionGranted() {
+ // SMS permission has been granted
+ Intent intent = new Intent(this, Login.class);
+ startActivity(intent);
+ finish();
+ }
+
+ private void permissionRejected() {
+ // SMS wasn't granted; skip to login anyway
+ Intent intent = new Intent(this, Login.class);
+ startActivity(intent);
+ finish();
+ }
+
+}
diff --git a/Project/app/src/main/java/edu/snhu/cs360/ccook/project/SetGoal.java b/Project/app/src/main/java/edu/snhu/cs360/ccook/project/SetGoal.java
new file mode 100644
index 0000000..473d800
--- /dev/null
+++ b/Project/app/src/main/java/edu/snhu/cs360/ccook/project/SetGoal.java
@@ -0,0 +1,93 @@
+package edu.snhu.cs360.ccook.project;
+
+import android.app.Dialog;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.RadioGroup;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+
+public class SetGoal extends DialogFragment {
+
+ private EditText weightInput;
+ private RadioGroup weightUnitGroup;
+ private Database dbHelper;
+ private boolean toLose;
+
+ public static SetGoal newInstance(int userId) {
+ // configure a new fragment with the user id so it can be saved to the database
+ SetGoal fragment = new SetGoal();
+ Bundle args = new Bundle();
+ args.putInt("userId", userId);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ // start a new database connection
+ dbHelper = new Database(getActivity());
+
+ // build view and layout for fragment dialog
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ LayoutInflater inflater = requireActivity().getLayoutInflater();
+ View view = inflater.inflate(R.layout.set_goal, null);
+
+ // identify the fields in the layout
+ weightInput = view.findViewById(R.id.weight_input);
+ weightUnitGroup = view.findViewById(R.id.weight_unit_group);
+
+ builder.setView(view).setMessage("Set Goal").setPositiveButton("Set", (dialog, id) -> {
+ // validate and reparse the widgets; goals don't alter weight table
+ validateAndAddWeight();
+ ((Home) getActivity()).setBannerWidgets(getArguments().getInt("userId", -1));
+ }).setNegativeButton("Cancel", (dialog, id) -> {
+ // cancel out if desired
+ dismiss();
+ });
+ // return dialog box
+ return builder.create();
+ }
+
+
+ private void validateAndAddWeight() {
+ assert getArguments() != null;
+ SQLiteDatabase db = dbHelper.getWritableDatabase();
+ int userId = getArguments().getInt("userId", -1);
+ String weightStr = weightInput.getText().toString();
+ double weight = Double.parseDouble(weightStr);
+ String weightUnit = getString(R.string.lb);
+ boolean isKg = weightUnitGroup.getCheckedRadioButtonId() == R.id.unit_kg;
+
+ if (isKg) {
+ weightUnit = getString(R.string.kg);
+ }
+
+ if (weight != 0) {
+ if (isKg) {
+ if (weight < 20 || weight > 430) { // Converted values for 45 lbs and 950 lbs
+ Toast.makeText(getContext(), "Weight must be between 20 kg and 430 kg", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ } else {
+ if (weight < 45 || weight > 950) {
+ Toast.makeText(getContext(), "Weight must be between 45 lbs and 950 lbs", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ }
+ // Add weight to database
+ dbHelper.setWeightGoal(db, userId, weight, weightUnit);
+ } else {
+ dbHelper.clearWeightGoal(db, userId);
+ }
+ }
+}
+
diff --git a/Project/app/src/main/java/edu/snhu/cs360/ccook/project/WeightEntry.java b/Project/app/src/main/java/edu/snhu/cs360/ccook/project/WeightEntry.java
new file mode 100644
index 0000000..edba796
--- /dev/null
+++ b/Project/app/src/main/java/edu/snhu/cs360/ccook/project/WeightEntry.java
@@ -0,0 +1,38 @@
+package edu.snhu.cs360.ccook.project;
+
+public class WeightEntry {
+ private final int userId;
+ private final String date;
+ private final double weight;
+ private final String unit;
+ private final int id;
+
+ public WeightEntry(int id, int userId, String date, double weight, String unit) {
+ this.id = id;
+ this.userId = userId;
+ this.date = date;
+ this.weight = weight;
+ this.unit = unit;
+ }
+
+ public String getDate() {
+ return date;
+ }
+
+ public double getWeight() {
+ return weight;
+ }
+
+ public String getWeightUnit() {
+ return unit;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public int getUserId() {
+ return userId;
+ }
+
+}
diff --git a/Project/app/src/main/java/edu/snhu/cs360/ccook/project/WeightEntryAdapter.java b/Project/app/src/main/java/edu/snhu/cs360/ccook/project/WeightEntryAdapter.java
new file mode 100644
index 0000000..10b0205
--- /dev/null
+++ b/Project/app/src/main/java/edu/snhu/cs360/ccook/project/WeightEntryAdapter.java
@@ -0,0 +1,60 @@
+package edu.snhu.cs360.ccook.project;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.List;
+
+public class WeightEntryAdapter extends RecyclerView.Adapter {
+ private final List weightEntries;
+ private final OnEntryInteractionListener listener;
+
+ public WeightEntryAdapter(List weightEntries, OnEntryInteractionListener listener) {
+ this.weightEntries = weightEntries;
+ this.listener = listener;
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.weight_items, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ WeightEntry entry = weightEntries.get(position);
+ holder.dateTextView.setText(entry.getDate());
+ holder.weightTextView.setText(entry.getWeight() + " " + entry.getWeightUnit());
+ holder.editIcon.setOnClickListener(v -> listener.onEditClicked(entry));
+ holder.deleteIcon.setOnClickListener(v -> listener.onDeleteClicked(entry));
+ }
+
+ @Override
+ public int getItemCount() {
+ return weightEntries.size();
+ }
+
+ public interface OnEntryInteractionListener {
+ void onEditClicked(WeightEntry entry);
+
+ void onDeleteClicked(WeightEntry entry);
+ }
+
+ static class ViewHolder extends RecyclerView.ViewHolder {
+ TextView dateTextView, weightTextView;
+ ImageView editIcon, deleteIcon;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+ dateTextView = itemView.findViewById(R.id.dateTextView);
+ weightTextView = itemView.findViewById(R.id.weightTextView);
+ editIcon = itemView.findViewById(R.id.editIcon);
+ deleteIcon = itemView.findViewById(R.id.deleteIcon);
+ }
+ }
+}
diff --git a/Project/app/src/main/play_store_512.png b/Project/app/src/main/play_store_512.png
new file mode 100644
index 0000000..f8e8989
Binary files /dev/null and b/Project/app/src/main/play_store_512.png differ
diff --git a/Project/app/src/main/res/drawable/border.xml b/Project/app/src/main/res/drawable/border.xml
new file mode 100644
index 0000000..186ce2a
--- /dev/null
+++ b/Project/app/src/main/res/drawable/border.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/Project/app/src/main/res/drawable/grid_item_background.xml b/Project/app/src/main/res/drawable/grid_item_background.xml
new file mode 100644
index 0000000..c3199f6
--- /dev/null
+++ b/Project/app/src/main/res/drawable/grid_item_background.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/Project/app/src/main/res/drawable/home.xml b/Project/app/src/main/res/drawable/home.xml
new file mode 100644
index 0000000..a58fd40
--- /dev/null
+++ b/Project/app/src/main/res/drawable/home.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/Project/app/src/main/res/drawable/ic_launcher_background.xml b/Project/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/Project/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Project/app/src/main/res/drawable/ic_launcher_foreground.xml b/Project/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/Project/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Project/app/src/main/res/drawable/notebook.xml b/Project/app/src/main/res/drawable/notebook.xml
new file mode 100644
index 0000000..8d27366
--- /dev/null
+++ b/Project/app/src/main/res/drawable/notebook.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/Project/app/src/main/res/drawable/scale.png b/Project/app/src/main/res/drawable/scale.png
new file mode 100644
index 0000000..4fc4bb4
Binary files /dev/null and b/Project/app/src/main/res/drawable/scale.png differ
diff --git a/Project/app/src/main/res/drawable/settings.xml b/Project/app/src/main/res/drawable/settings.xml
new file mode 100644
index 0000000..7d41625
--- /dev/null
+++ b/Project/app/src/main/res/drawable/settings.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/Project/app/src/main/res/drawable/sortasc.xml b/Project/app/src/main/res/drawable/sortasc.xml
new file mode 100644
index 0000000..b3c0370
--- /dev/null
+++ b/Project/app/src/main/res/drawable/sortasc.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/Project/app/src/main/res/drawable/sortdesc.xml b/Project/app/src/main/res/drawable/sortdesc.xml
new file mode 100644
index 0000000..fa685f1
--- /dev/null
+++ b/Project/app/src/main/res/drawable/sortdesc.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/Project/app/src/main/res/layout/home.xml b/Project/app/src/main/res/layout/home.xml
new file mode 100644
index 0000000..dfd0156
--- /dev/null
+++ b/Project/app/src/main/res/layout/home.xml
@@ -0,0 +1,185 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Project/app/src/main/res/layout/login.xml b/Project/app/src/main/res/layout/login.xml
new file mode 100644
index 0000000..2488280
--- /dev/null
+++ b/Project/app/src/main/res/layout/login.xml
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Project/app/src/main/res/layout/record_weight.xml b/Project/app/src/main/res/layout/record_weight.xml
new file mode 100644
index 0000000..7a6314a
--- /dev/null
+++ b/Project/app/src/main/res/layout/record_weight.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Project/app/src/main/res/layout/set_goal.xml b/Project/app/src/main/res/layout/set_goal.xml
new file mode 100644
index 0000000..7a6314a
--- /dev/null
+++ b/Project/app/src/main/res/layout/set_goal.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Project/app/src/main/res/layout/sms.xml b/Project/app/src/main/res/layout/sms.xml
new file mode 100644
index 0000000..5debaef
--- /dev/null
+++ b/Project/app/src/main/res/layout/sms.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Project/app/src/main/res/layout/weight_items.xml b/Project/app/src/main/res/layout/weight_items.xml
new file mode 100644
index 0000000..b58ab69
--- /dev/null
+++ b/Project/app/src/main/res/layout/weight_items.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Project/app/src/main/res/menu/add.xml b/Project/app/src/main/res/menu/add.xml
new file mode 100644
index 0000000..029c760
--- /dev/null
+++ b/Project/app/src/main/res/menu/add.xml
@@ -0,0 +1,12 @@
+
+
diff --git a/Project/app/src/main/res/menu/mainmenu.xml b/Project/app/src/main/res/menu/mainmenu.xml
new file mode 100644
index 0000000..2f38b81
--- /dev/null
+++ b/Project/app/src/main/res/menu/mainmenu.xml
@@ -0,0 +1,15 @@
+
+
diff --git a/Project/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/Project/app/src/main/res/mipmap-anydpi/ic_launcher.xml
new file mode 100644
index 0000000..345888d
--- /dev/null
+++ b/Project/app/src/main/res/mipmap-anydpi/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Project/app/src/main/res/mipmap-hdpi/ic_launcher.png b/Project/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..735ed4b
Binary files /dev/null and b/Project/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/Project/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/Project/app/src/main/res/mipmap-hdpi/ic_launcher_background.png
new file mode 100644
index 0000000..1966948
Binary files /dev/null and b/Project/app/src/main/res/mipmap-hdpi/ic_launcher_background.png differ
diff --git a/Project/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/Project/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..5e81280
Binary files /dev/null and b/Project/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/Project/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png b/Project/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000..e0d80d2
Binary files /dev/null and b/Project/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png differ
diff --git a/Project/app/src/main/res/mipmap-mdpi/ic_launcher.png b/Project/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..072ea4b
Binary files /dev/null and b/Project/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/Project/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/Project/app/src/main/res/mipmap-mdpi/ic_launcher_background.png
new file mode 100644
index 0000000..75025cf
Binary files /dev/null and b/Project/app/src/main/res/mipmap-mdpi/ic_launcher_background.png differ
diff --git a/Project/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/Project/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..01bba17
Binary files /dev/null and b/Project/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/Project/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png b/Project/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000..29858fe
Binary files /dev/null and b/Project/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png differ
diff --git a/Project/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/Project/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..2609ff9
Binary files /dev/null and b/Project/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/Project/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/Project/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
new file mode 100644
index 0000000..9784f16
Binary files /dev/null and b/Project/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png differ
diff --git a/Project/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/Project/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..4b7fe0c
Binary files /dev/null and b/Project/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/Project/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png b/Project/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000..938d694
Binary files /dev/null and b/Project/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png differ
diff --git a/Project/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/Project/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..25ef360
Binary files /dev/null and b/Project/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/Project/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/Project/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
new file mode 100644
index 0000000..04ef206
Binary files /dev/null and b/Project/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png differ
diff --git a/Project/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/Project/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..1df4513
Binary files /dev/null and b/Project/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/Project/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png b/Project/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000..b507fd9
Binary files /dev/null and b/Project/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png differ
diff --git a/Project/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/Project/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..3373d65
Binary files /dev/null and b/Project/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/Project/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/Project/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
new file mode 100644
index 0000000..66a5487
Binary files /dev/null and b/Project/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png differ
diff --git a/Project/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/Project/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..4049f09
Binary files /dev/null and b/Project/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/Project/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png b/Project/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000..9aa4a4b
Binary files /dev/null and b/Project/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png differ
diff --git a/Project/app/src/main/res/values-night/themes.xml b/Project/app/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..9f7e0c8
--- /dev/null
+++ b/Project/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/Project/app/src/main/res/values/colors.xml b/Project/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..e95d4fb
--- /dev/null
+++ b/Project/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #FF000000
+ @android:color/system_secondary_container_light
+ #FFFFFFFF
+
diff --git a/Project/app/src/main/res/values/strings.xml b/Project/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..2e02b44
--- /dev/null
+++ b/Project/app/src/main/res/values/strings.xml
@@ -0,0 +1,74 @@
+
+
+ Add New Data
+ "Adding weight for user_id "
+ Weight-Tracking App
+ Authentication failed.
+ Date
+ "Checking database for user "
+ Change Settings
+ Congrats!
+ Congrats on reaching your goal!
+ Congrats on reaching your goal, we know you could do it.
+ Creating new database with tables
+ Current Weight:
+ Database
+ app.db
+ MM/dd/yy
+ Date
+ Delete
+ Cody Cook
+ Edit
+ Edit Weight
+ Enter weight
+ Error initializing database
+ Failed to login user
+ Failed to register user
+ Go Back
+ Goal Met
+ Goal Weight:
+ "Grabbing weights in for user_id "
+ Home
+ kg
+ lb
+ Log into your account, or register a new one.
+ Login
+ Login error.
+ Login successful!
+ LoginActivity
+ Lose/Gain
+ Password
+ +15551234567
+ Set Goal
+ Record Weight
+ Register
+ "Registering new user "
+ Registration failed.
+ Registration successful!
+ Check SMS Permissions
+ Settings
+ SMS failed to send, please try again.
+ We request SMS permissions to notify you about recording your weight and when you reach your goal weight.
+ Sort\n
+ Sort by Date
+ Stats
+ The app\'s logo.
+ To Gain:
+ To Lose:
+ Unset
+ Upgrading database due to version change; wiping data.
+ Username
+ "Validating password for user "
+ weight
+ 2024–04–01
+ Weight
+ Weight must be between 20 kg and 430 kg
+ Weight must be between 45 lbs and 950 lbs
+ Weight
+ Your Weight Journey
+ Save
+ userId
+ Cancel
+ entryId
+ isKg
+
diff --git a/Project/app/src/main/res/values/themes.xml b/Project/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..733d78d
--- /dev/null
+++ b/Project/app/src/main/res/values/themes.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Project/app/src/main/res/xml/backup_rules.xml b/Project/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..fa0f996
--- /dev/null
+++ b/Project/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/Project/app/src/main/res/xml/data_extraction_rules.xml b/Project/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/Project/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Project/app/src/test/java/edu/snhu/cs360/ccook/project/ExampleUnitTest.java b/Project/app/src/test/java/edu/snhu/cs360/ccook/project/ExampleUnitTest.java
new file mode 100644
index 0000000..6d82d68
--- /dev/null
+++ b/Project/app/src/test/java/edu/snhu/cs360/ccook/project/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package edu.snhu.cs360.ccook.project;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/Project/build.gradle.kts b/Project/build.gradle.kts
new file mode 100644
index 0000000..2bbd2a9
--- /dev/null
+++ b/Project/build.gradle.kts
@@ -0,0 +1,4 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.androidApplication) apply false
+}
\ No newline at end of file
diff --git a/Project/gradle.properties b/Project/gradle.properties
new file mode 100644
index 0000000..4387edc
--- /dev/null
+++ b/Project/gradle.properties
@@ -0,0 +1,21 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/Project/gradle/libs.versions.toml b/Project/gradle/libs.versions.toml
new file mode 100644
index 0000000..b2c581f
--- /dev/null
+++ b/Project/gradle/libs.versions.toml
@@ -0,0 +1,22 @@
+[versions]
+agp = "8.3.2"
+junit = "4.13.2"
+junitVersion = "1.1.5"
+espressoCore = "3.5.1"
+appcompat = "1.6.1"
+material = "1.11.0"
+activity = "1.9.0"
+constraintlayout = "2.1.4"
+
+[libraries]
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
+constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+
+[plugins]
+androidApplication = { id = "com.android.application", version.ref = "agp" }
+
diff --git a/Project/gradle/wrapper/gradle-wrapper.jar b/Project/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/Project/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/Project/gradle/wrapper/gradle-wrapper.properties b/Project/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..6d27600
--- /dev/null
+++ b/Project/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sat Apr 20 15:39:45 PDT 2024
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/Project/gradlew b/Project/gradlew
new file mode 100644
index 0000000..4f906e0
--- /dev/null
+++ b/Project/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/Project/gradlew.bat b/Project/gradlew.bat
new file mode 100644
index 0000000..ac1b06f
--- /dev/null
+++ b/Project/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/Project/settings.gradle.kts b/Project/settings.gradle.kts
new file mode 100644
index 0000000..f94907e
--- /dev/null
+++ b/Project/settings.gradle.kts
@@ -0,0 +1,24 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "Project"
+include(":app")
+
\ No newline at end of file