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 @@ + + + + + + + + + + + + + + + + + + + + + + +