This commit is contained in:
2026-02-25 09:43:28 +00:00
parent efb6281c78
commit b83a105c4c
762 changed files with 54838 additions and 25802 deletions

View File

@@ -6,6 +6,11 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Notificações e Alarmes -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -35,6 +40,8 @@
<activity android:name=".ui.auth.RegisterActivity" />
<activity android:name=".ui.auth.ForgotPasswordActivity" />
<receiver android:name=".services.AlarmReceiver" android:exported="false" />
</application>
</manifest>

View File

@@ -12,6 +12,13 @@ import com.example.cuida.databinding.ActivityMainBinding;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.example.cuida.ui.auth.LoginActivity;
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Build;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.example.cuida.utils.NotificationHelper;
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
@@ -20,6 +27,9 @@ public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Initialize Notification Channels
new NotificationHelper(this);
// Check if user is logged in
boolean isLoggedIn = getSharedPreferences("prefs", Context.MODE_PRIVATE)
.getBoolean("is_logged_in", false);
@@ -31,6 +41,15 @@ public class MainActivity extends AppCompatActivity {
return;
}
// Check for Notification Permission on Android 13+ after ensuring user is
// logged in
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[] { Manifest.permission.POST_NOTIFICATIONS }, 101);
}
}
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());

View File

@@ -0,0 +1,29 @@
package com.example.cuida.services;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import com.example.cuida.utils.NotificationHelper;
public class AlarmReceiver extends BroadcastReceiver {
private static final String TAG = "AlarmReceiver";
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null) {
String title = intent.getStringExtra("EXTRA_TITLE");
String message = intent.getStringExtra("EXTRA_MESSAGE");
int notificationId = intent.getIntExtra("EXTRA_NOTIFICATION_ID", (int) System.currentTimeMillis());
Log.d(TAG, "Alarm received! Title: " + title + " Msg: " + message);
NotificationHelper notificationHelper = new NotificationHelper(context);
if (title != null && title.contains("Medicamento")) {
notificationHelper.sendNotification(title, message, notificationId, "MEDICATION_CHANNEL_ID");
} else {
notificationHelper.sendNotification(title, message, notificationId, "APPOINTMENT_CHANNEL_ID");
}
}
}
}

View File

@@ -9,6 +9,13 @@ import com.example.cuida.data.dao.UserDao;
import com.example.cuida.data.model.User;
import com.example.cuida.databinding.ActivityRegisterBinding;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.firestore.FirebaseFirestore;
import java.util.HashMap;
import java.util.Map;
public class RegisterActivity extends AppCompatActivity {
private ActivityRegisterBinding binding;
@@ -43,27 +50,24 @@ public class RegisterActivity extends AppCompatActivity {
binding.registerButton.setEnabled(false);
binding.registerButton.setText("A registar...");
com.google.firebase.auth.FirebaseAuth mAuth = com.google.firebase.auth.FirebaseAuth.getInstance();
com.google.firebase.firestore.FirebaseFirestore db = com.google.firebase.firestore.FirebaseFirestore
.getInstance();
FirebaseAuth mAuth = FirebaseAuth.getInstance();
FirebaseFirestore db = FirebaseFirestore.getInstance();
mAuth.createUserWithEmailAndPassword(email, password)
.addOnCompleteListener(this, task -> {
if (task.isSuccessful()) {
// Registration success, save additional info to Firestore
com.google.firebase.auth.FirebaseUser firebaseUser = mAuth.getCurrentUser();
FirebaseUser firebaseUser = mAuth.getCurrentUser();
if (firebaseUser != null) {
String userId = firebaseUser.getUid();
java.util.Map<String, Object> userMap = new java.util.HashMap<>();
userMap.put("uid", userId);
userMap.put("name", name);
Map<String, Object> userMap = new HashMap<>();
userMap.put("nome_completo", name);
userMap.put("idade", ageStr);
userMap.put("numero_utente", utenteStr);
userMap.put("email", email);
userMap.put("age", age);
userMap.put("utenteNumber", utenteStr);
userMap.put("profilePictureUri", ""); // Init empty
db.collection("users").document(userId)
db.collection("utilizadores").document(userId)
.set(userMap)
.addOnSuccessListener(aVoid -> {
// Optional: Also save to local Room DB for offline cache if desired,

View File

@@ -1,7 +1,5 @@
package com.example.cuida.ui.medication;
import static android.os.Build.VERSION_CODES.R;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

View File

@@ -19,7 +19,8 @@ import java.util.Locale;
public class MedicationDialog extends DialogFragment {
private EditText editName, editNotes;
private android.widget.CheckBox checkOral, checkTopical, checkInhalatory;
private android.widget.RadioButton radioOral, radioTopical, radioInhalatory;
private android.widget.RadioGroup radioGroupRoute;
private TextView textTime;
private Medication medicationToEdit;
private OnMedicationSaveListener listener;
@@ -47,9 +48,10 @@ public class MedicationDialog extends DialogFragment {
editNotes = view.findViewById(R.id.edit_med_notes);
textTime = view.findViewById(R.id.text_med_time);
checkOral = view.findViewById(R.id.checkbox_oral);
checkTopical = view.findViewById(R.id.checkbox_topical);
checkInhalatory = view.findViewById(R.id.checkbox_inhalatory);
radioGroupRoute = view.findViewById(R.id.radio_group_route);
radioOral = view.findViewById(R.id.radio_oral);
radioTopical = view.findViewById(R.id.radio_topical);
radioInhalatory = view.findViewById(R.id.radio_inhalatory);
// Set up TimePicker
textTime.setOnClickListener(v -> showTimePicker());
@@ -59,12 +61,14 @@ public class MedicationDialog extends DialogFragment {
editNotes.setText(medicationToEdit.notes);
textTime.setText(medicationToEdit.time);
// Parse dosage string to set checkboxes
String dosage = medicationToEdit.dosage;
if (dosage != null) {
checkOral.setChecked(dosage.contains("Oral"));
checkTopical.setChecked(dosage.contains("Tópica"));
checkInhalatory.setChecked(dosage.contains("Inalatória"));
if (dosage.contains("Oral"))
radioOral.setChecked(true);
else if (dosage.contains("Tópica"))
radioTopical.setChecked(true);
else if (dosage.contains("Inalatória"))
radioInhalatory.setChecked(true);
}
builder.setTitle("Editar Medicamento");
@@ -81,19 +85,15 @@ public class MedicationDialog extends DialogFragment {
String notes = editNotes.getText().toString();
String time = textTime.getText().toString();
StringBuilder dosageBuilder = new StringBuilder();
if (checkOral.isChecked())
dosageBuilder.append("Via Oral, ");
if (checkTopical.isChecked())
dosageBuilder.append("Via Tópica, ");
if (checkInhalatory.isChecked())
dosageBuilder.append("Via Inalatória, ");
int selectedId = radioGroupRoute.getCheckedRadioButtonId();
String dosage = "Via não especificada";
String dosage = dosageBuilder.toString();
if (dosage.endsWith(", ")) {
dosage = dosage.substring(0, dosage.length() - 2);
} else if (dosage.isEmpty()) {
dosage = "Via não especificada";
if (selectedId == R.id.radio_oral) {
dosage = "Via Oral";
} else if (selectedId == R.id.radio_topical) {
dosage = "Via Tópica";
} else if (selectedId == R.id.radio_inhalatory) {
dosage = "Via Inalatória";
}
if (medicationToEdit != null) {

View File

@@ -55,6 +55,33 @@ public class MedicationFragment extends Fragment {
} else {
medicationViewModel.update(medicationToSave);
}
try {
String[] timeParts = medicationToSave.time.split(":");
int hour = Integer.parseInt(timeParts[0]);
int minute = Integer.parseInt(timeParts[1]);
java.util.Calendar calendar = java.util.Calendar.getInstance();
calendar.set(java.util.Calendar.HOUR_OF_DAY, hour);
calendar.set(java.util.Calendar.MINUTE, minute);
calendar.set(java.util.Calendar.SECOND, 0);
if (calendar.getTimeInMillis() <= System.currentTimeMillis()) {
calendar.add(java.util.Calendar.DAY_OF_YEAR, 1);
}
String title = "Hora do Medicamento";
String msg = "É hora de tomar: " + medicationToSave.name + " (" + medicationToSave.dosage + ")";
com.example.cuida.utils.AlarmScheduler.scheduleAlarm(
requireContext(),
calendar.getTimeInMillis(),
title,
msg,
medicationToSave.name.hashCode());
} catch (Exception e) {
e.printStackTrace();
}
dialog.dismiss();
});
dialog.show(getParentFragmentManager(), "MedicationDialog");

View File

@@ -118,6 +118,7 @@ public class ProfileFragment extends Fragment {
EditText editEmail = dialogView.findViewById(R.id.edit_email);
dialogImageView = dialogView.findViewById(R.id.edit_profile_image);
View btnChangePhoto = dialogView.findViewById(R.id.button_change_photo);
View btnChangePassword = dialogView.findViewById(R.id.button_change_password);
View btnSave = dialogView.findViewById(R.id.button_save);
View btnCancel = dialogView.findViewById(R.id.button_cancel);
@@ -133,6 +134,7 @@ public class ProfileFragment extends Fragment {
}
dialogImageView.setOnClickListener(v -> pickMedia.launch("image/*"));
btnChangePhoto.setOnClickListener(v -> pickMedia.launch("image/*"));
btnChangePassword.setOnClickListener(v -> {
showChangePasswordDialog();
@@ -163,22 +165,23 @@ public class ProfileFragment extends Fragment {
AppDatabase.databaseWriteExecutor.execute(() -> {
userDao.insert(currentUser);
// Update SharedPreferences if email changed (key for login persistence)
if (emailChanged) {
getContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
.edit()
.putString("user_email", newEmail)
.apply();
}
getActivity().runOnUiThread(() -> {
// UI update
loadUserData(); // Reload to show new image and data
Toast.makeText(getContext(), "Dados atualizados com sucesso!", Toast.LENGTH_SHORT).show();
dialog.dismiss();
dialogImageView = null; // Clear reference
});
});
// Update SharedPreferences if email changed (key for login persistence)
if (emailChanged) {
getContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
.edit()
.putString("user_email", newEmail)
.apply();
}
// UI update
loadUserData(); // Reload to show new image and data
Toast.makeText(getContext(), "Dados atualizados com sucesso!", Toast.LENGTH_SHORT).show();
dialog.dismiss();
dialogImageView = null; // Clear reference
});
btnCancel.setOnClickListener(v -> {

View File

@@ -103,6 +103,36 @@ public class ScheduleViewModel extends AndroidViewModel {
Appointment appointment = new Appointment("Consulta Geral", date, time, reason, false);
executorService.execute(() -> {
appointmentDao.insert(appointment);
try {
String[] dateParts = date.split("/");
int day = Integer.parseInt(dateParts[0]);
int month = Integer.parseInt(dateParts[1]) - 1; // 0-based
int year = Integer.parseInt(dateParts[2]);
String[] timeParts = time.split(":");
int hour = Integer.parseInt(timeParts[0]);
int minute = Integer.parseInt(timeParts[1]);
java.util.Calendar calendar = java.util.Calendar.getInstance();
calendar.set(year, month, day, hour, minute, 0);
// 1 hour before
calendar.add(java.util.Calendar.HOUR_OF_DAY, -1);
if (calendar.getTimeInMillis() > System.currentTimeMillis()) {
String title = "Lembrete de Consulta";
String msg = "A sua consulta é daqui a 1 hora (" + time + ").";
com.example.cuida.utils.AlarmScheduler.scheduleAlarm(
getApplication(),
calendar.getTimeInMillis(),
title,
msg,
(date + time).hashCode());
}
} catch (Exception e) {
e.printStackTrace();
}
saveSuccess.postValue(true);
});
}

View File

@@ -0,0 +1,48 @@
package com.example.cuida.utils;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import com.example.cuida.services.AlarmReceiver;
public class AlarmScheduler {
public static void scheduleAlarm(Context context, long timeInMillis, String title, String message,
int requestCode) {
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (alarmManager == null)
return;
Intent intent = new Intent(context, AlarmReceiver.class);
intent.putExtra("EXTRA_TITLE", title);
intent.putExtra("EXTRA_MESSAGE", message);
intent.putExtra("EXTRA_NOTIFICATION_ID", requestCode);
PendingIntent pendingIntent = PendingIntent.getBroadcast(
context,
requestCode,
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (alarmManager.canScheduleExactAlarms()) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);
} else {
// Fallback to inexact alarm if exact permission is revoked
alarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);
} else {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);
}
} catch (SecurityException e) {
// Android 14+ requires explicit consent for SCHEDULE_EXACT_ALARM except for
// clocks/calendars
// Fallback when security exception is raised
alarmManager.set(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);
}
}
}

View File

@@ -0,0 +1,74 @@
package com.example.cuida.utils;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.os.Build;
import androidx.core.app.NotificationCompat;
import com.example.cuida.MainActivity;
import com.example.cuida.R;
public class NotificationHelper {
private final Context context;
public static final String MEDICATION_CHANNEL_ID = "MEDICATION_CHANNEL_ID";
public static final String APPOINTMENT_CHANNEL_ID = "APPOINTMENT_CHANNEL_ID";
public NotificationHelper(Context context) {
this.context = context;
createChannels();
}
private void createChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager manager = context.getSystemService(NotificationManager.class);
if (manager != null) {
// Medication Channel
NotificationChannel medChannel = new NotificationChannel(
MEDICATION_CHANNEL_ID,
"Lembretes de Medicação",
NotificationManager.IMPORTANCE_HIGH);
medChannel.setDescription("Notificações para tomar a medicação a horas");
medChannel.enableLights(true);
medChannel.setLightColor(Color.BLUE);
manager.createNotificationChannel(medChannel);
// Appointment Channel
NotificationChannel apptChannel = new NotificationChannel(
APPOINTMENT_CHANNEL_ID,
"Lembretes de Consultas",
NotificationManager.IMPORTANCE_HIGH);
apptChannel.setDescription("Notificações antes das consultas");
apptChannel.enableLights(true);
apptChannel.setLightColor(Color.GREEN);
manager.createNotificationChannel(apptChannel);
}
}
}
public void sendNotification(String title, String message, int notificationId, String channelId) {
Intent intent = new Intent(context, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_launcher_final) // Using app icon
.setContentTitle(title)
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setContentIntent(pendingIntent);
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (manager != null) {
manager.notify(notificationId, builder.build());
}
}
}

View File

@@ -43,24 +43,30 @@
android:textSize="14sp"
android:layout_marginBottom="4dp"/>
<CheckBox
android:id="@+id/checkbox_oral"
<RadioGroup
android:id="@+id/radio_group_route"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Via Oral (Pela boca)" />
android:layout_marginBottom="16dp">
<CheckBox
android:id="@+id/checkbox_topical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Via Tópica (Na pele)" />
<RadioButton
android:id="@+id/radio_oral"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Via Oral (Pela boca)" />
<CheckBox
android:id="@+id/checkbox_inhalatory"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Via Inalatória (Pelo nariz/boca)"
android:layout_marginBottom="16dp"/>
<RadioButton
android:id="@+id/radio_topical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Via Tópica (Na pele)" />
<RadioButton
android:id="@+id/radio_inhalatory"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Via Inalatória (Pelo nariz/boca)" />
</RadioGroup>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
@@ -9,16 +10,27 @@
android:orientation="vertical"
android:padding="24dp">
<ImageView
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/edit_profile_image"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/ic_placeholder"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="16dp"
android:layout_marginBottom="8dp"
android:scaleType="centerCrop"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.CornerSize50Percent"
android:clickable="true"
android:focusable="true"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_change_photo"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Mudar Foto"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View File

@@ -8,11 +8,13 @@
android:gravity="center_horizontal"
android:background="@color/background_color">
<ImageView
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/profile_image"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/ic_placeholder"
android:scaleType="centerCrop"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.CornerSize50Percent"
android:layout_marginBottom="24dp"/>
<com.google.android.material.card.MaterialCardView

View File

@@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Cuida" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<style name="Theme.Cuida" parent="Theme.MaterialComponents.Light.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/primary_color</item>
<item name="colorPrimaryVariant">@color/primary_dark_color</item>
@@ -33,4 +33,8 @@
<item name="strokeColor">#E0E0E0</item>
<item name="cardBackgroundColor">@color/surface_color</item>
</style>
<style name="ShapeAppearanceOverlay.App.CornerSize50Percent" parent="">
<item name="cornerSize">50%</item>
</style>
</resources>