This commit is contained in:
2026-04-28 16:13:43 +01:00
parent 25f1743baf
commit 5d413e0f8a
116 changed files with 7671 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<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" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<application
android:name=".CuidaApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@drawable/ic_launcher_final"
android:label="@string/app_name"
android:roundIcon="@drawable/ic_launcher_final"
android:supportsRtl="true"
android:theme="@style/Theme.Cuida"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize">
<!-- Launcher Intent Filter handled in LoginActivity usually, but for now we might start Main -->
</activity>
<activity android:name=".ui.auth.LoginActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ui.auth.RegisterActivity" />
<activity android:name=".ui.auth.ForgotPasswordActivity" />
<activity android:name=".ui.auth.ResetPasswordActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Update the host to match your Firebase project's hosting domain
or the custom domain you configured for dynamic links -->
<data
android:scheme="https"
android:host="papcuida.page.link" />
<data
android:scheme="cuida"
android:host="resetpassword" />
</intent-filter>
</activity>
<receiver android:name=".services.AlarmReceiver" android:exported="false" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

View File

@@ -0,0 +1,19 @@
package com.example.cuida;
import android.app.Application;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.FirebaseFirestoreSettings;
public class CuidaApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// Ativar persistência offline globalmente para toda a aplicação
FirebaseFirestore db = FirebaseFirestore.getInstance();
FirebaseFirestoreSettings settings = new FirebaseFirestoreSettings.Builder()
.setPersistenceEnabled(true)
.build();
db.setFirestoreSettings(settings);
}
}

View File

@@ -0,0 +1,64 @@
package com.example.cuida;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;
import com.example.cuida.databinding.ActivityMainBinding;
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;
@Override
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);
if (!isLoggedIn) {
Intent intent = new Intent(this, LoginActivity.class);
startActivity(intent);
finish();
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());
// Find Navigation Host Fragment and setup Bottom Navigation
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager()
.findFragmentById(R.id.nav_host_fragment);
if (navHostFragment != null) {
NavController navController = navHostFragment.getNavController();
NavigationUI.setupWithNavController(binding.navView, navController);
}
}
}

View File

@@ -0,0 +1,31 @@
package com.example.cuida.data.model;
import com.google.firebase.firestore.DocumentId;
public class Appointment {
@DocumentId
public String id;
public String type; // e.g. "Medicina Geral", "Cardiologia"
public String date; // dd/MM/yyyy
public String time; // HH:mm
public String reason;
public boolean isPast;
public String userId;
public String status; // "Pendente", "Aceite", "Rejeitada"
// Required empty constructor for Firestore deserialization
public Appointment() {
}
public Appointment(String type, String date, String time, String reason, boolean isPast, String userId, String status) {
this.type = type;
this.date = date;
this.time = time;
this.reason = reason;
this.isPast = isPast;
this.userId = userId;
this.status = status;
}
}

View File

@@ -0,0 +1,18 @@
package com.example.cuida.data.model;
public class Comprimido {
public String nome;
public String dosagem;
public Comprimido() {}
public Comprimido(String nome, String dosagem) {
this.nome = nome;
this.dosagem = dosagem;
}
@Override
public String toString() {
return nome;
}
}

View File

@@ -0,0 +1,76 @@
package com.example.cuida.data.model;
import com.google.firebase.firestore.PropertyName;
public class Medication {
private String id;
@PropertyName("nome")
public String name;
@PropertyName("hora")
public String time;
@PropertyName("dosagem")
public String dosage;
@PropertyName("notas")
public String notes;
public boolean isTaken;
public String userId;
public Medication() {
// Obrigatório para o Firestore
}
public Medication(String name, String time, String dosage, String notes, String userId) {
this.name = name;
this.time = time;
this.dosage = dosage;
this.notes = notes;
this.isTaken = false;
this.userId = userId;
}
// --- Getters e Setters com compatibilidade para nomes antigos (name, time, dosage, notes) ---
@PropertyName("nome")
public String getName() { return name; }
@PropertyName("nome")
public void setName(String name) { this.name = name; }
@PropertyName("name") // Suporte para dados antigos
public void setNameOld(String name) { if (this.name == null) this.name = name; }
@PropertyName("hora")
public String getTime() { return time; }
@PropertyName("hora")
public void setTime(String time) { this.time = time; }
@PropertyName("time") // Suporte para dados antigos
public void setTimeOld(String time) { if (this.time == null) this.time = time; }
@PropertyName("dosagem")
public String getDosage() { return dosage; }
@PropertyName("dosagem")
public void setDosage(String dosage) { this.dosage = dosage; }
@PropertyName("dosage") // Suporte para dados antigos
public void setDosageOld(String dosage) { if (this.dosage == null) this.dosage = dosage; }
@PropertyName("notas")
public String getNotes() { return notes; }
@PropertyName("notas")
public void setNotes(String notes) { this.notes = notes; }
@PropertyName("notes") // Suporte para dados antigos
public void setNotesOld(String notes) { if (this.notes == null) this.notes = notes; }
public String getId() { return id; }
public void setId(String id) { this.id = id; }
}

View File

@@ -0,0 +1,82 @@
package com.example.cuida.data.model;
import com.google.gson.annotations.SerializedName;
public class Perfil {
@SerializedName("id")
private String id;
@SerializedName("nome_completo")
private String nome_completo;
@SerializedName("idade")
private int idade;
@SerializedName("numero_utente")
private String numero_utente;
@SerializedName("gmail")
private String gmail;
@SerializedName("sexo")
private String sexo;
public Perfil(String id, String nome, int idade, String utente, String email, String sexo) {
this.id = id;
this.nome_completo = nome;
this.idade = idade;
this.numero_utente = utente;
this.gmail = email;
this.sexo = sexo;
}
// Getters
public String getId() {
return id;
}
public String getNomeCompleto() {
return nome_completo;
}
public int getIdade() {
return idade;
}
public String getNumeroUtente() {
return numero_utente;
}
public String getGmail() {
return gmail;
}
public String getSexo() {
return sexo;
}
// Setters
public void setId(String id) {
this.id = id;
}
public void setNomeCompleto(String nome_completo) {
this.nome_completo = nome_completo;
}
public void setIdade(int idade) {
this.idade = idade;
}
public void setNumeroUtente(String numero_utente) {
this.numero_utente = numero_utente;
}
public void setGmail(String gmail) {
this.gmail = gmail;
}
public void setSexo(String sexo) {
this.sexo = sexo;
}
}

View File

@@ -0,0 +1,64 @@
package com.example.cuida.data.model;
import com.google.firebase.firestore.DocumentId;
public class User {
@DocumentId
@com.google.firebase.firestore.Exclude
public String id;
@com.google.firebase.firestore.PropertyName("nome_completo")
public String name;
@com.google.firebase.firestore.PropertyName("email")
public String email;
@com.google.firebase.firestore.Exclude
public String password;
@com.google.firebase.firestore.PropertyName("idade")
public int age;
@com.google.firebase.firestore.PropertyName("numero_utente")
public String utenteNumber;
@com.google.firebase.firestore.PropertyName("profilePictureUri")
public String profilePictureUri;
@com.google.firebase.firestore.PropertyName("tipo")
public String tipo = "paciente";
// Required empty constructor for Firestore deserialization
public User() {
}
public User(String name, String email, String password, int age, String utenteNumber) {
this.name = name;
this.email = email;
this.password = password;
this.age = age;
this.utenteNumber = utenteNumber;
}
@com.google.firebase.firestore.PropertyName("nome_completo")
public String getName() { return name; }
@com.google.firebase.firestore.PropertyName("nome_completo")
public void setName(String name) { this.name = name; }
@com.google.firebase.firestore.PropertyName("idade")
public int getAge() { return age; }
@com.google.firebase.firestore.PropertyName("idade")
public void setAge(int age) { this.age = age; }
@com.google.firebase.firestore.PropertyName("numero_utente")
public String getUtenteNumber() { return utenteNumber; }
@com.google.firebase.firestore.PropertyName("numero_utente")
public void setUtenteNumber(String utenteNumber) { this.utenteNumber = utenteNumber; }
@com.google.firebase.firestore.PropertyName("tipo")
public String getTipo() { return tipo; }
@com.google.firebase.firestore.PropertyName("tipo")
public void setTipo(String tipo) { this.tipo = tipo; }
}

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

@@ -0,0 +1,89 @@
package com.example.cuida.services;
import android.os.Handler;
import android.os.Looper;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class Gemini {
private static final String API_KEY = "AIzaSyBYar6Yv0rhrZX8cIQQxd77TLERHRsjAtY";
private static final String MODEL_NAME = "gemini-2.0-flash";
private static final String API_URL = "https://generativelanguage.googleapis.com/v1beta/models/" + MODEL_NAME + ":generateContent?key=" + API_KEY;
private final OkHttpClient client;
private final Handler mainHandler;
public Gemini() {
this.client = new OkHttpClient();
this.mainHandler = new Handler(Looper.getMainLooper());
}
public interface GeminiCallback {
void onSuccess(String result);
void onError(Throwable t);
}
public void fazerPergunta(String promptUtilizador, GeminiCallback callback) {
try {
JSONObject jsonBody = new JSONObject();
JSONArray contents = new JSONArray();
JSONObject content = new JSONObject();
JSONArray parts = new JSONArray();
JSONObject part = new JSONObject();
part.put("text", promptUtilizador);
parts.put(part);
content.put("parts", parts);
contents.put(content);
jsonBody.put("contents", contents);
RequestBody body = RequestBody.create(jsonBody.toString(), MediaType.parse("application/json; charset=utf-8"));
Request request = new Request.Builder()
.url(API_URL)
.post(body)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
mainHandler.post(() -> callback.onError(e));
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
try {
String responseBody = response.body().string();
JSONObject jsonObject = new JSONObject(responseBody);
JSONArray candidates = jsonObject.getJSONArray("candidates");
JSONObject firstCandidate = candidates.getJSONObject(0);
JSONObject contentObj = firstCandidate.getJSONObject("content");
JSONArray partsArr = contentObj.getJSONArray("parts");
String textResult = partsArr.getJSONObject(0).getString("text");
mainHandler.post(() -> callback.onSuccess(textResult));
} catch (Exception e) {
mainHandler.post(() -> callback.onError(new Exception("Erro ao ler resposta da IA", e)));
}
} else {
String errorBody = response.body() != null ? response.body().string() : "Unknown error";
mainHandler.post(() -> callback.onError(new Exception("Erro da API HTTP " + response.code() + ": " + errorBody)));
}
}
});
} catch (Exception e) {
mainHandler.post(() -> callback.onError(e));
}
}
}

View File

@@ -0,0 +1,71 @@
package com.example.cuida.ui.appointments;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.example.cuida.R;
import com.example.cuida.data.model.Appointment;
import java.util.ArrayList;
import java.util.List;
public class AppointmentAdapter extends RecyclerView.Adapter<AppointmentAdapter.AppointmentViewHolder> {
private List<Appointment> appointmentList = new ArrayList<>();
public void setAppointments(List<Appointment> appointments) {
this.appointmentList = appointments;
notifyDataSetChanged();
}
@NonNull
@Override
public AppointmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_appointment, parent, false);
return new AppointmentViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull AppointmentViewHolder holder, int position) {
Appointment appointment = appointmentList.get(position);
holder.textType.setText(appointment.type);
holder.textDate.setText(appointment.date);
holder.textTime.setText(appointment.time);
holder.textReason.setText("Motivo: " + (appointment.reason != null ? appointment.reason : "--"));
String status = appointment.status != null ? appointment.status : "Pendente";
holder.textStatus.setText(status);
if ("Aceite".equalsIgnoreCase(status)) {
holder.textStatus.setTextColor(android.graphics.Color.parseColor("#388E3C")); // Green
holder.textStatus.setBackgroundColor(android.graphics.Color.parseColor("#C8E6C9"));
} else if ("Rejeitada".equalsIgnoreCase(status)) {
holder.textStatus.setTextColor(android.graphics.Color.parseColor("#D32F2F")); // Red
holder.textStatus.setBackgroundColor(android.graphics.Color.parseColor("#FFCDD2"));
} else {
holder.textStatus.setTextColor(android.graphics.Color.parseColor("#F57C00")); // Orange
holder.textStatus.setBackgroundColor(android.graphics.Color.parseColor("#FFE0B2"));
holder.textStatus.setText("Pendente");
}
}
@Override
public int getItemCount() {
return appointmentList.size();
}
public static class AppointmentViewHolder extends RecyclerView.ViewHolder {
TextView textType, textDate, textTime, textReason, textStatus;
public AppointmentViewHolder(@NonNull View itemView) {
super(itemView);
textType = itemView.findViewById(R.id.text_type);
textDate = itemView.findViewById(R.id.text_date);
textTime = itemView.findViewById(R.id.text_time);
textReason = itemView.findViewById(R.id.text_reason);
textStatus = itemView.findViewById(R.id.text_status);
}
}
}

View File

@@ -0,0 +1,51 @@
package com.example.cuida.ui.appointments;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.example.cuida.databinding.FragmentAppointmentsBinding;
public class AppointmentsFragment extends Fragment {
private FragmentAppointmentsBinding binding;
private AppointmentsViewModel appointmentsViewModel;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
appointmentsViewModel = new ViewModelProvider(this).get(AppointmentsViewModel.class);
binding = FragmentAppointmentsBinding.inflate(inflater, container, false);
// Future Appointments
AppointmentAdapter futureAdapter = new AppointmentAdapter();
binding.recyclerAppointmentsFuture.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerAppointmentsFuture.setAdapter(futureAdapter);
appointmentsViewModel.getFutureAppointments().observe(getViewLifecycleOwner(), appointments -> {
futureAdapter.setAppointments(appointments);
});
// Past Appointments
AppointmentAdapter pastAdapter = new AppointmentAdapter();
binding.recyclerAppointmentsPast.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerAppointmentsPast.setAdapter(pastAdapter);
appointmentsViewModel.getPastAppointments().observe(getViewLifecycleOwner(), appointments -> {
pastAdapter.setAppointments(appointments);
});
return binding.getRoot();
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View File

@@ -0,0 +1,139 @@
package com.example.cuida.ui.appointments;
import android.app.Application;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.example.cuida.data.model.Appointment;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.Query;
import com.google.firebase.firestore.QueryDocumentSnapshot;
import java.util.ArrayList;
import java.util.List;
public class AppointmentsViewModel extends AndroidViewModel {
private final MutableLiveData<List<Appointment>> futureAppointments = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<Appointment>> pastAppointments = new MutableLiveData<>(new ArrayList<>());
private final FirebaseFirestore db;
private final FirebaseAuth auth;
public AppointmentsViewModel(@NonNull Application application) {
super(application);
db = FirebaseFirestore.getInstance();
auth = FirebaseAuth.getInstance();
fetchAppointments();
}
private void fetchAppointments() {
if (auth.getCurrentUser() == null)
return;
String userId = auth.getCurrentUser().getUid();
// 1. Fetch Future Appointments
db.collection("consultas")
.whereEqualTo("userId", userId)
.whereEqualTo("isPast", false)
.addSnapshotListener((value, error) -> {
if (error != null) {
Log.e("AppointmentsVM", "Listen failed for future.", error);
return;
}
List<Appointment> apps = new ArrayList<>();
java.util.Date now = new java.util.Date();
java.text.SimpleDateFormat format = new java.text.SimpleDateFormat("dd/MM/yyyy HH:mm",
java.util.Locale.getDefault());
if (value != null) {
for (QueryDocumentSnapshot doc : value) {
Appointment app = doc.toObject(Appointment.class);
try {
java.util.Date appDate = format.parse(app.date + " " + app.time);
if (appDate != null && appDate.before(now)) {
// It passed out of date. Update in DB to move it to Past Appointments.
db.collection("consultas").document(doc.getId()).update("isPast", true);
} else {
apps.add(app);
}
} catch (java.text.ParseException e) {
apps.add(app);
}
}
}
// Sort locally
apps.sort((a, b) -> {
try {
java.util.Date dateA = format.parse(a.date + " " + a.time);
java.util.Date dateB = format.parse(b.date + " " + b.time);
return dateA.compareTo(dateB);
} catch (java.text.ParseException e) {
return 0;
}
});
futureAppointments.setValue(apps);
});
// 2. Fetch Past Appointments
db.collection("consultas")
.whereEqualTo("userId", userId)
.whereEqualTo("isPast", true)
.addSnapshotListener((value, error) -> {
if (error != null) {
Log.e("AppointmentsVM", "Listen failed for past.", error);
return;
}
List<Appointment> apps = new ArrayList<>();
if (value != null) {
for (QueryDocumentSnapshot doc : value) {
apps.add(doc.toObject(Appointment.class));
}
}
// Sort locally descending
java.text.SimpleDateFormat format = new java.text.SimpleDateFormat("dd/MM/yyyy HH:mm",
java.util.Locale.getDefault());
apps.sort((a, b) -> {
try {
java.util.Date dateA = format.parse(a.date + " " + a.time);
java.util.Date dateB = format.parse(b.date + " " + b.time);
if (dateA == null || dateB == null) return 0;
return dateB.compareTo(dateA); // Reverse for descending
} catch (java.text.ParseException e) {
return 0;
}
});
pastAppointments.setValue(apps);
});
}
public LiveData<List<Appointment>> getFutureAppointments() {
return futureAppointments;
}
public LiveData<List<Appointment>> getPastAppointments() {
return pastAppointments;
}
public void insert(Appointment appointment) {
if (auth.getCurrentUser() == null)
return;
String userId = auth.getCurrentUser().getUid();
appointment.userId = userId;
db.collection("consultas")
.add(appointment)
.addOnSuccessListener(documentReference -> Log.d("AppointmentsVM", "Appointment added"))
.addOnFailureListener(e -> Log.w("AppointmentsVM", "Error adding appointment", e));
}
}

View File

@@ -0,0 +1,46 @@
package com.example.cuida.ui.auth;
import android.os.Bundle;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.example.cuida.databinding.ActivityForgotPasswordBinding;
public class ForgotPasswordActivity extends AppCompatActivity {
private ActivityForgotPasswordBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityForgotPasswordBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
binding.resetButton.setOnClickListener(v -> {
String email = binding.emailEditText.getText().toString().trim();
if (email.isEmpty()) {
Toast.makeText(this, "Insira o seu email.", Toast.LENGTH_SHORT).show();
} else {
// Real Firebase Password Reset Logic
com.google.firebase.auth.FirebaseAuth.getInstance().sendPasswordResetEmail(email)
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
Toast.makeText(this, "Email enviado!", Toast.LENGTH_SHORT).show();
finish();
} else {
String errorMsg = task.getException() != null ? task.getException().getMessage() : "Erro desconhecido";
if (errorMsg != null) {
if (errorMsg.contains("There is no user record")) {
errorMsg = "Não existe conta associada a este email.";
} else if (errorMsg.contains("badly formatted")) {
errorMsg = "O formato do email é inválido.";
}
}
Toast.makeText(this, "Erro: " + errorMsg, Toast.LENGTH_LONG).show();
}
});
}
});
binding.backToLogin.setOnClickListener(v -> finish());
}
}

View File

@@ -0,0 +1,273 @@
package com.example.cuida.ui.auth;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import com.example.cuida.MainActivity;
import com.example.cuida.data.model.User;
import com.example.cuida.databinding.ActivityLoginBinding;
import com.example.cuida.R;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import androidx.biometric.BiometricPrompt;
import androidx.biometric.BiometricManager;
import androidx.core.content.ContextCompat;
import java.util.concurrent.Executor;
public class LoginActivity extends AppCompatActivity {
// gvjhbk
private ActivityLoginBinding binding;
private FirebaseAuth mAuth;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Initialize Firebase Auth
mAuth = FirebaseAuth.getInstance();
// Check if user is already logged in and wants to be remembered
SharedPreferences prefs = getSharedPreferences("prefs", MODE_PRIVATE);
boolean isLoggedIn = prefs.getBoolean("is_logged_in", false);
boolean rememberMe = prefs.getBoolean("remember_me", false);
if (isLoggedIn && rememberMe) {
if (mAuth.getCurrentUser() != null) {
startActivity(new Intent(this, MainActivity.class));
finish();
return;
}
} else {
// Se não for para lembrar a sessão, garantimos que o estado de login é falso
// mas NÃO limpamos as credenciais guardadas para a biometria.
if (mAuth.getCurrentUser() != null) {
mAuth.signOut();
}
prefs.edit().putBoolean("is_logged_in", false).apply();
}
binding = ActivityLoginBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
binding.loginButton.setOnClickListener(v -> login());
binding.registerLink.setOnClickListener(v -> {
startActivity(new Intent(this, RegisterActivity.class));
finish();
});
binding.forgotPasswordLink.setOnClickListener(v -> {
startActivity(new Intent(this, ForgotPasswordActivity.class));
});
setupBiometrics();
}
private void setupBiometrics() {
BiometricManager biometricManager = BiometricManager.from(this);
int canAuthenticate = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL);
SharedPreferences prefs = getSharedPreferences("prefs", MODE_PRIVATE);
String savedEmail = prefs.getString("saved_email", null);
String savedPass = prefs.getString("saved_pass", null);
if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS && savedEmail != null && savedPass != null) {
binding.biometricButton.setVisibility(android.view.View.VISIBLE);
binding.biometricButton.setOnClickListener(v -> showBiometricPrompt(savedEmail, savedPass));
}
}
private void showBiometricPrompt(String email, String pass) {
Executor executor = ContextCompat.getMainExecutor(this);
BiometricPrompt biometricPrompt = new BiometricPrompt(LoginActivity.this, executor, new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
}
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
// Perform login with saved credentials
loginWithSavedCredentials(email, pass);
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
}
});
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle("Autenticação Biométrica")
.setSubtitle("Entre na sua conta usando biometria")
.setNegativeButtonText("Usar Password")
.build();
biometricPrompt.authenticate(promptInfo);
}
private void loginWithSavedCredentials(String email, String pass) {
if (!isNetworkAvailable()) {
Toast.makeText(this, "Sem ligação à internet. Verifique a sua rede e tente novamente.", Toast.LENGTH_LONG).show();
return;
}
binding.loginButton.setEnabled(false);
binding.loginButton.setText("A entrar...");
mAuth.signInWithEmailAndPassword(email, pass)
.addOnCompleteListener(this, task -> {
if (task.isSuccessful()) {
FirebaseUser user = mAuth.getCurrentUser();
if (user != null) {
handleSuccessfulAuth(user, email, pass, true);
}
} else {
binding.loginButton.setEnabled(true);
binding.loginButton.setText(R.string.login_button);
String errorMsg = traduzirErroFirebase(task.getException());
Toast.makeText(this, "Erro: " + errorMsg, Toast.LENGTH_SHORT).show();
}
});
}
private boolean isNetworkAvailable() {
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm == null) return false;
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
return activeNetwork != null && activeNetwork.isConnected();
}
private void login() {
String email = binding.emailEditText.getText().toString().trim();
String password = binding.passwordEditText.getText().toString();
if (email.isEmpty() || password.isEmpty()) {
Toast.makeText(this, "Preencha os campos.", Toast.LENGTH_SHORT).show();
return;
}
if (!isNetworkAvailable()) {
Toast.makeText(this, "Sem internet.", Toast.LENGTH_SHORT).show();
return;
}
binding.loginButton.setEnabled(false);
binding.loginButton.setText("A entrar...");
mAuth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener(this, task -> {
if (task.isSuccessful()) {
FirebaseUser user = mAuth.getCurrentUser();
if (user != null) {
boolean rememberMe = binding.checkboxRememberMe.isChecked();
handleSuccessfulAuth(user, email, password, rememberMe);
}
} else {
binding.loginButton.setEnabled(true);
binding.loginButton.setText(R.string.login_button);
String errorMsg = traduzirErroFirebase(task.getException());
Toast.makeText(LoginActivity.this, errorMsg, Toast.LENGTH_LONG).show();
}
});
}
private String traduzirErroFirebase(Exception exception) {
if (exception == null) return "Erro desconhecido.";
String className = exception.getClass().getSimpleName();
if (className.contains("Network") || className.contains("IOException")) {
return "Sem internet.";
}
String msg = exception.getMessage();
if (msg == null) return "Erro desconhecido.";
if (msg.contains("NETWORK_REQUEST_FAILED") || msg.contains("network error")
|| msg.contains("network") || msg.contains("Network")) {
return "Sem internet.";
} else if (msg.contains("invalid credential") || msg.contains("password is invalid")
|| msg.contains("There is no user record") || msg.contains("INVALID_LOGIN_CREDENTIALS")) {
return "Credenciais erradas.";
} else if (msg.contains("badly formatted")) {
return "Email inválido.";
} else if (msg.contains("too many requests") || msg.contains("TOO_MANY_ATTEMPTS_TRY_LATER")) {
return "Tente mais tarde.";
} else if (msg.contains("user disabled") || msg.contains("USER_DISABLED")) {
return "Conta desativada.";
}
return "Erro ao entrar.";
}
private void handleSuccessfulAuth(FirebaseUser user, String email, String password, boolean rememberMe) {
SharedPreferences prefs = getSharedPreferences("prefs", MODE_PRIVATE);
prefs.edit().putBoolean("is_logged_in", true).apply();
prefs.edit().putBoolean("remember_me", rememberMe).apply();
// Guardar sempre para biometria após login com sucesso
prefs.edit().putString("saved_email", email).apply();
prefs.edit().putString("saved_pass", password).apply();
// Tentar primeiro na coleção 'utilizadores'
com.google.firebase.firestore.FirebaseFirestore db = com.google.firebase.firestore.FirebaseFirestore.getInstance();
db.collection("utilizadores").document(user.getUid()).get()
.addOnCompleteListener(task -> {
if (task.isSuccessful() && task.getResult() != null && task.getResult().exists()) {
com.google.firebase.firestore.DocumentSnapshot doc = task.getResult();
String name = doc.getString("name");
String dbEmail = doc.getString("email");
if (name != null) prefs.edit().putString("user_name", name).apply();
if (dbEmail != null) prefs.edit().putString("user_email", dbEmail).apply();
proceedToMain();
} else {
// Tentar na coleção 'medicos'
db.collection("medicos").document(user.getUid()).get()
.addOnCompleteListener(task2 -> {
if (task2.isSuccessful() && task2.getResult() != null && task2.getResult().exists()) {
com.google.firebase.firestore.DocumentSnapshot doc = task2.getResult();
String name = doc.getString("nome"); // Notar campo 'nome' em vez de 'name'
String dbEmail = doc.getString("email");
if (name != null) prefs.edit().putString("user_name", name).apply();
if (dbEmail != null) prefs.edit().putString("user_email", dbEmail).apply();
proceedToMain();
} else {
// Fallback se não encontrar em lado nenhum
prefs.edit().putString("user_email", user.getEmail()).apply();
if (user.getDisplayName() != null && !user.getDisplayName().isEmpty()) {
prefs.edit().putString("user_name", user.getDisplayName()).apply();
} else {
String authEmail = user.getEmail();
if (authEmail != null && authEmail.contains("@")) {
String fallbackName = authEmail.substring(0, authEmail.indexOf("@"));
prefs.edit().putString("user_name", fallbackName).apply();
}
}
proceedToMain();
}
});
}
});
}
private void proceedToMain() {
Toast.makeText(LoginActivity.this, "Bem-vindo!", Toast.LENGTH_SHORT).show();
startActivity(new Intent(LoginActivity.this, com.example.cuida.MainActivity.class));
finish();
}
}

View File

@@ -0,0 +1,118 @@
package com.example.cuida.ui.auth;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
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;
public class RegisterActivity extends AppCompatActivity {
private ActivityRegisterBinding binding;
private FirebaseAuth mAuth;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityRegisterBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
binding.registerButton.setOnClickListener(v -> register());
binding.loginLink.setOnClickListener(v -> {
startActivity(new Intent(this, LoginActivity.class));
finish();
});
String[] genders = new String[]{"Masculino", "Feminino"};
android.widget.ArrayAdapter<String> adapter = new android.widget.ArrayAdapter<>(
this, android.R.layout.simple_dropdown_item_1line, genders);
binding.genderAutoComplete.setAdapter(adapter);
}
private void register() {
String name = binding.nameEditText.getText().toString();
String ageStr = binding.ageEditText.getText().toString();
String utenteStr = binding.utenteEditText.getText().toString();
String email = binding.emailEditText.getText().toString();
String password = binding.passwordEditText.getText().toString();
String gender = binding.genderAutoComplete.getText().toString();
if (name.isEmpty() || ageStr.isEmpty() || email.isEmpty() || password.isEmpty() || utenteStr.isEmpty() || gender.isEmpty()) {
Toast.makeText(this, "Preencha os campos.", Toast.LENGTH_SHORT).show();
return;
}
if (utenteStr.length() != 9) {
Toast.makeText(this, "Utente deve ter 9 dígitos.", Toast.LENGTH_SHORT).show();
return;
}
int age = Integer.parseInt(ageStr);
binding.registerButton.setEnabled(false);
binding.registerButton.setText("A registar...");
mAuth = FirebaseAuth.getInstance();
FirebaseFirestore db = FirebaseFirestore
.getInstance();
mAuth.createUserWithEmailAndPassword(email, password)
.addOnCompleteListener(this, task -> {
if (task.isSuccessful()) {
// Registration success, save additional info to Firestore
FirebaseUser firebaseUser = mAuth.getCurrentUser();
if (firebaseUser != null) {
saveUserData(firebaseUser.getUid(), name, email, ageStr, utenteStr, gender);
}
} else {
Exception e = task.getException();
if (e instanceof com.google.firebase.auth.FirebaseAuthUserCollisionException) {
// Tenta fazer login automático para reparar o perfil se ele não existir no Firestore
mAuth.signInWithEmailAndPassword(email, password)
.addOnSuccessListener(authResult -> {
saveUserData(authResult.getUser().getUid(), name, email, ageStr, utenteStr, gender);
})
.addOnFailureListener(err -> {
binding.registerButton.setEnabled(true);
binding.registerButton.setText("Registar");
Toast.makeText(RegisterActivity.this, "Email já registado.", Toast.LENGTH_SHORT).show();
});
} else {
binding.registerButton.setEnabled(true);
binding.registerButton.setText("Registar");
String errorMsg = e != null ? e.getMessage() : "Erro desconhecido";
Toast.makeText(RegisterActivity.this, "Erro: " + errorMsg, Toast.LENGTH_LONG).show();
}
}
});
}
private void saveUserData(String userId, String name, String email, String ageStr, String utenteStr, String gender) {
FirebaseFirestore db = FirebaseFirestore.getInstance();
java.util.Map<String, Object> userMap = new java.util.HashMap<>();
userMap.put("id", userId);
userMap.put("nome_completo", name);
userMap.put("email", email);
userMap.put("idade", ageStr);
userMap.put("numero_utente", utenteStr);
userMap.put("sexo", gender);
userMap.put("tipo", "paciente");
userMap.put("profilePictureUri", "");
db.collection("utilizadores").document(userId)
.set(userMap)
.addOnSuccessListener(aVoid -> {
Toast.makeText(RegisterActivity.this, "Conta criada!", Toast.LENGTH_SHORT).show();
startActivity(new Intent(RegisterActivity.this, LoginActivity.class));
finish();
})
.addOnFailureListener(e -> {
binding.registerButton.setEnabled(true);
binding.registerButton.setText("Registar");
Toast.makeText(RegisterActivity.this, "Erro ao guardar dados.", Toast.LENGTH_SHORT).show();
});
}
}

View File

@@ -0,0 +1,82 @@
package com.example.cuida.ui.auth;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.example.cuida.databinding.ActivityResetPasswordBinding;
import com.google.firebase.auth.FirebaseAuth;
public class ResetPasswordActivity extends AppCompatActivity {
private ActivityResetPasswordBinding binding;
private String oobCode;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityResetPasswordBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Use custom uri scheme or https scheme, extracting oobCode parameter
Intent intent = getIntent();
if (intent != null && intent.getData() != null) {
Uri data = intent.getData();
oobCode = data.getQueryParameter("oobCode");
if (oobCode == null || oobCode.isEmpty()) {
Toast.makeText(this, "Link inválido.", Toast.LENGTH_SHORT).show();
finish();
}
} else {
Toast.makeText(this, "Código não encontrado.", Toast.LENGTH_SHORT).show();
finish();
}
binding.saveNewPasswordButton.setOnClickListener(v -> saveNewPassword());
}
private void saveNewPassword() {
String newPassword = binding.newPasswordEditText.getText().toString();
String confirmPassword = binding.confirmNewPasswordEditText.getText().toString();
if (newPassword.isEmpty() || confirmPassword.isEmpty()) {
Toast.makeText(this, "Preencha as passwords.", Toast.LENGTH_SHORT).show();
return;
}
if (!newPassword.equals(confirmPassword)) {
Toast.makeText(this, "Pass não coincidem.", Toast.LENGTH_SHORT).show();
return;
}
if (newPassword.length() < 6) {
Toast.makeText(this, "Mínimo 6 caracteres.", Toast.LENGTH_SHORT).show();
return;
}
binding.saveNewPasswordButton.setEnabled(false);
binding.saveNewPasswordButton.setText("A guardar...");
FirebaseAuth.getInstance().confirmPasswordReset(oobCode, newPassword)
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
Toast.makeText(this, "Pass atualizada!", Toast.LENGTH_SHORT).show();
// Go back to login screen
Intent intent = new Intent(ResetPasswordActivity.this, LoginActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finish();
} else {
binding.saveNewPasswordButton.setEnabled(true);
binding.saveNewPasswordButton.setText("Guardar Palavra-passe");
String errorMsg = task.getException() != null ? task.getException().getMessage() : "Erro desconhecido";
Toast.makeText(this, "Erro: " + errorMsg, Toast.LENGTH_LONG).show();
}
});
}
}

View File

@@ -0,0 +1,95 @@
package com.example.cuida.ui.home;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import com.example.cuida.databinding.FragmentHomeBinding;
import com.example.cuida.ui.medication.MedicationViewModel;
import com.example.cuida.ui.appointments.AppointmentsViewModel;
import com.example.cuida.data.model.Appointment;
import java.util.Calendar;
import java.util.Locale;
public class HomeFragment extends Fragment {
private FragmentHomeBinding binding;
private MedicationViewModel medicationViewModel;
private AppointmentsViewModel appointmentsViewModel;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
binding = FragmentHomeBinding.inflate(inflater, container, false);
// --- Greeting & Profile Picture ---
com.google.firebase.auth.FirebaseAuth auth = com.google.firebase.auth.FirebaseAuth.getInstance();
if (auth.getCurrentUser() != null) {
String userId = auth.getCurrentUser().getUid();
com.google.firebase.firestore.FirebaseFirestore.getInstance().collection("utilizadores").document(userId)
.get()
.addOnSuccessListener(documentSnapshot -> {
if (documentSnapshot.exists() && isAdded()) {
// Tenta 'nome_completo' (novo) ou 'name' (antigo)
String name = documentSnapshot.getString("nome_completo");
if (name == null || name.isEmpty()) name = documentSnapshot.getString("name");
if (name != null && !name.isEmpty()) {
// Extract first name
String firstName = name.split(" ")[0];
binding.textGreeting.setText("Olá, " + firstName + "!");
} else {
binding.textGreeting.setText("Olá, Utilizador!");
}
// Load Profile Picture
String profilePictureUri = documentSnapshot.getString("profilePictureUri");
if (profilePictureUri != null && !profilePictureUri.isEmpty()) {
try {
binding.imageProfileHome.setImageURI(android.net.Uri.parse(profilePictureUri));
} catch (Exception e) {
android.util.Log.e("HomeFragment", "Error loading profile pic view: " + e.getMessage());
}
}
}
})
.addOnFailureListener(e -> {
if (isAdded())
binding.textGreeting.setText("Olá, Utilizador!");
});
} else {
binding.textGreeting.setText("Olá, Utilizador!");
}
// --- Next Medication ---
medicationViewModel = new ViewModelProvider(this).get(MedicationViewModel.class);
medicationViewModel.getNextMedication().observe(getViewLifecycleOwner(), medication -> {
if (medication != null) {
binding.nextMedName.setText(medication.name + " (" + medication.dosage + ")");
binding.nextMedTime.setText("Hoje, " + medication.time);
} else {
binding.nextMedName.setText("Sem medicação");
binding.nextMedTime.setText("--:--");
}
});
// --- Book Appointment ---
appointmentsViewModel = new ViewModelProvider(this).get(AppointmentsViewModel.class);
binding.buttonBookAppointment.setOnClickListener(v -> {
androidx.navigation.Navigation.findNavController(v)
.navigate(com.example.cuida.R.id.action_home_to_schedule_appointment);
});
return binding.getRoot();
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View File

@@ -0,0 +1,54 @@
package com.example.cuida.ui.medication;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.example.cuida.R;
import com.example.cuida.data.model.Comprimido;
import java.util.List;
public class ComprimidoRecyclerAdapter extends RecyclerView.Adapter<ComprimidoRecyclerAdapter.ViewHolder> {
private List<Comprimido> pills;
private OnItemClickListener listener;
public interface OnItemClickListener {
void onItemClick(Comprimido comprimido);
}
public ComprimidoRecyclerAdapter(List<Comprimido> pills, OnItemClickListener listener) {
this.pills = pills;
this.listener = listener;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_comprimido_search, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Comprimido pill = pills.get(position);
holder.textName.setText(pill.nome);
holder.itemView.setOnClickListener(v -> listener.onItemClick(pill));
}
@Override
public int getItemCount() {
return pills.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView textName;
public ViewHolder(@NonNull View itemView) {
super(itemView);
textName = itemView.findViewById(R.id.text_pill_name);
}
}
}

View File

@@ -0,0 +1,85 @@
package com.example.cuida.ui.medication;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.example.cuida.R;
import com.example.cuida.data.model.Medication;
import java.util.ArrayList;
import java.util.List;
public class MedicationAdapter extends RecyclerView.Adapter<MedicationAdapter.MedicationViewHolder> {
private List<Medication> medicationList = new ArrayList<>();
private final OnItemClickListener listener;
public interface OnItemClickListener {
void onCheckClick(Medication medication);
void onItemClick(Medication medication);
}
public MedicationAdapter(OnItemClickListener listener) {
this.listener = listener;
}
public void setMedications(List<Medication> medications) {
this.medicationList = medications;
notifyDataSetChanged();
}
@NonNull
@Override
public MedicationViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_medication, parent, false);
return new MedicationViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull MedicationViewHolder holder, int position) {
Medication medication = medicationList.get(position);
holder.textName.setText(medication.name);
holder.textDosage.setText(medication.dosage);
holder.textTime.setText(medication.time);
holder.textNotes.setText(medication.notes);
// Remove listener temporarily to avoid triggering it during bind
holder.checkBoxTaken.setOnCheckedChangeListener(null);
holder.checkBoxTaken.setChecked(medication.isTaken);
holder.checkBoxTaken.setOnCheckedChangeListener((buttonView, isChecked) -> {
medication.isTaken = isChecked;
listener.onCheckClick(medication);
});
}
@Override
public int getItemCount() {
return medicationList.size();
}
public class MedicationViewHolder extends RecyclerView.ViewHolder {
TextView textName, textDosage, textTime, textNotes;
CheckBox checkBoxTaken;
public MedicationViewHolder(@NonNull View itemView) {
super(itemView);
textName = itemView.findViewById(R.id.text_med_name);
textDosage = itemView.findViewById(R.id.text_med_dosage);
textTime = itemView.findViewById(R.id.text_med_time);
textNotes = itemView.findViewById(R.id.text_med_notes);
checkBoxTaken = itemView.findViewById(R.id.checkbox_taken);
itemView.setOnClickListener(v -> {
int position = getAdapterPosition();
if (listener != null && position != RecyclerView.NO_POSITION) {
listener.onItemClick(medicationList.get(position));
}
});
}
}
}

View File

@@ -0,0 +1,390 @@
package com.example.cuida.ui.medication;
import android.app.Dialog;
import android.app.TimePickerDialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.textfield.TextInputEditText;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import com.example.cuida.R;
import com.example.cuida.data.model.Medication;
import java.util.Calendar;
import java.util.Locale;
import java.util.ArrayList;
import java.util.List;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;
import android.text.Editable;
import android.text.TextWatcher;
import com.example.cuida.data.model.Comprimido;
import android.widget.AdapterView;
import android.widget.Toast;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import com.google.android.material.button.MaterialButton;
import java.util.Collections;
public class MedicationDialog extends DialogFragment {
private TextInputEditText editName;
private RecyclerView recyclerResults;
private ComprimidoRecyclerAdapter recyclerAdapter;
private List<Comprimido> searchResults = new ArrayList<>();
private List<Comprimido> fullPillsList = new ArrayList<>();
private DatabaseReference medicationRef;
private EditText editNotes;
private android.widget.RadioButton radioOral, radioTopical, radioInhalatory;
private android.widget.RadioGroup radioGroupRoute;
private ChipGroup chipGroupTimes;
private List<String> selectedTimes = new ArrayList<>();
private Medication medicationToEdit;
private OnMedicationSaveListener listener;
private OnMedicationDeleteListener deleteListener;
public interface OnMedicationSaveListener {
void onSave(Medication medication);
}
public interface OnMedicationDeleteListener {
void onDelete(Medication medication);
}
public void setListener(OnMedicationSaveListener listener) {
this.listener = listener;
}
public void setDeleteListener(OnMedicationDeleteListener listener) {
this.deleteListener = listener;
}
public void setMedicationToEdit(Medication medication) {
this.medicationToEdit = medication;
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
LayoutInflater inflater = requireActivity().getLayoutInflater();
View view = inflater.inflate(R.layout.dialog_add_medication, null);
editName = view.findViewById(R.id.edit_med_name);
recyclerResults = view.findViewById(R.id.recycler_search_results);
editNotes = view.findViewById(R.id.edit_med_notes);
chipGroupTimes = view.findViewById(R.id.chip_group_times);
MaterialButton btnAddTime = view.findViewById(R.id.btn_add_time);
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);
final android.content.Context currentContext = getContext();
if (currentContext != null) {
recyclerAdapter = new ComprimidoRecyclerAdapter(searchResults, selected -> {
editName.setText(selected.nome);
editName.setSelection(selected.nome.length());
// Adiciona a dosagem/informação ao campo de notas automaticamente
if (selected.dosagem != null && !selected.dosagem.isEmpty()) {
editNotes.setText(selected.dosagem);
}
recyclerResults.setVisibility(View.GONE);
searchResults.clear();
});
recyclerResults.setLayoutManager(new LinearLayoutManager(currentContext));
recyclerResults.setAdapter(recyclerAdapter);
String dbUrl = "https://cuidamais-7b904-default-rtdb.firebaseio.com/";
medicationRef = FirebaseDatabase.getInstance(dbUrl).getReference("medication");
// Carregar todos os medicamentos uma única vez para filtragem local rápida
fetchAllMedsOnce();
editName.addTextChangedListener(new 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) {
filterMedsLocally(s.toString().trim());
}
@Override public void afterTextChanged(Editable s) {}
});
}
radioOral = view.findViewById(R.id.radio_oral);
radioTopical = view.findViewById(R.id.radio_topical);
radioInhalatory = view.findViewById(R.id.radio_inhalatory);
// Set up TimePicker
btnAddTime.setOnClickListener(v -> showTimePicker());
if (medicationToEdit != null) {
editName.setText(medicationToEdit.name);
editNotes.setText(medicationToEdit.notes);
if (medicationToEdit.time != null && !medicationToEdit.time.isEmpty()) {
String[] times = medicationToEdit.time.split(",\\s*");
for (String t : times) {
if (!t.isEmpty()) selectedTimes.add(t);
}
java.util.Collections.sort(selectedTimes);
refreshTimeChips();
}
String dosage = medicationToEdit.dosage;
if (dosage != null) {
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");
} else {
builder.setTitle("Adicionar Medicamento");
// Default time to current time
Calendar cal = Calendar.getInstance();
String defaultTime = String.format(Locale.getDefault(), "%02d:%02d", cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE));
selectedTimes.add(defaultTime);
refreshTimeChips();
}
// Positive button sem lógica — a validação é feita no setOnShowListener abaixo
builder.setView(view)
.setPositiveButton("Guardar", null);
if (medicationToEdit != null) {
builder.setNeutralButton("Eliminar", (dialog, id) -> {
if (deleteListener != null) {
deleteListener.onDelete(medicationToEdit);
}
});
}
AlertDialog alertDialog = builder.create();
alertDialog.setOnShowListener(d -> {
android.widget.Button btnPos = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
if (btnPos != null) {
// Estilo do botão Guardar
btnPos.setBackgroundResource(R.drawable.btn_outline_primary);
btnPos.setTextColor(androidx.core.content.ContextCompat.getColor(requireContext(), R.color.primary_color));
int paddingPx = (int) (16 * getResources().getDisplayMetrics().density);
btnPos.setPadding(paddingPx, 0, paddingPx, 0);
// Validação ao clicar em Guardar (impede o fecho automático do dialog)
btnPos.setOnClickListener(v -> {
String name = editName.getText() != null ? editName.getText().toString().trim() : "";
// 1. Nome obrigatório
if (name.isEmpty()) {
Toast.makeText(getContext(), "Preencha o nome.", Toast.LENGTH_SHORT).show();
editName.requestFocus();
return;
}
// 2. Via de administração obrigatória
int selectedId = radioGroupRoute.getCheckedRadioButtonId();
if (selectedId == -1) {
Toast.makeText(getContext(), "Selecione a via.", Toast.LENGTH_SHORT).show();
return;
}
// 3. Pelo menos um horário obrigatório
if (selectedTimes.isEmpty()) {
Toast.makeText(getContext(), "Adicione um horário.", Toast.LENGTH_SHORT).show();
return;
}
// Tudo válido — construir e guardar
String notes = editNotes.getText() != null ? editNotes.getText().toString() : "";
StringBuilder timeBuilder = new StringBuilder();
for (int i = 0; i < selectedTimes.size(); i++) {
timeBuilder.append(selectedTimes.get(i));
if (i < selectedTimes.size() - 1) timeBuilder.append(", ");
}
String time = timeBuilder.toString();
String dosage;
if (selectedId == R.id.radio_oral) {
dosage = "Via Oral";
} else if (selectedId == R.id.radio_topical) {
dosage = "Via Tópica";
} else {
dosage = "Via Inalatória";
}
if (medicationToEdit != null) {
medicationToEdit.name = name;
medicationToEdit.dosage = dosage;
medicationToEdit.notes = notes;
medicationToEdit.time = time;
if (listener != null) listener.onSave(medicationToEdit);
} else {
Medication newMed = new Medication(name, time, dosage, notes, null);
if (listener != null) listener.onSave(newMed);
}
alertDialog.dismiss();
});
}
android.widget.Button btnNeu = alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL);
if (btnNeu != null) {
btnNeu.setTextColor(androidx.core.content.ContextCompat.getColor(requireContext(), R.color.error_color));
btnNeu.setBackgroundResource(R.drawable.btn_outline_error);
int paddingPx = (int) (16 * getResources().getDisplayMetrics().density);
btnNeu.setPadding(paddingPx, 0, paddingPx, 0);
android.view.ViewGroup.LayoutParams lp = btnNeu.getLayoutParams();
if (lp instanceof android.view.ViewGroup.MarginLayoutParams) {
android.view.ViewGroup.MarginLayoutParams marginLp = (android.view.ViewGroup.MarginLayoutParams) lp;
int marginPx = (int) (8 * getResources().getDisplayMetrics().density);
marginLp.setMargins(marginLp.leftMargin, marginLp.topMargin, marginLp.rightMargin + marginPx, marginLp.bottomMargin);
btnNeu.setLayoutParams(marginLp);
}
}
});
return alertDialog;
}
private void showTimePicker() {
Calendar cal = Calendar.getInstance();
int hour = cal.get(Calendar.HOUR_OF_DAY);
int minute = cal.get(Calendar.MINUTE);
TimePickerDialog timePickerDialog = new TimePickerDialog(getContext(),
(view, hourOfDay, minute1) -> {
String time = String.format(Locale.getDefault(), "%02d:%02d", hourOfDay, minute1);
if (!selectedTimes.contains(time)) {
selectedTimes.add(time);
Collections.sort(selectedTimes);
refreshTimeChips();
}
},
hour, minute, true);
timePickerDialog.show();
}
private void refreshTimeChips() {
if (chipGroupTimes == null || getContext() == null) return;
chipGroupTimes.removeAllViews();
for (String time : selectedTimes) {
Chip chip = new Chip(getContext());
chip.setText(time);
chip.setCloseIconVisible(true);
chip.setOnCloseIconClickListener(v -> {
selectedTimes.remove(time);
refreshTimeChips();
});
chipGroupTimes.addView(chip);
}
}
private void fetchAllMedsOnce() {
String dbUrl = "https://cuidamais-7b904-default-rtdb.firebaseio.com/";
DatabaseReference rootRef = FirebaseDatabase.getInstance(dbUrl).getReference();
String[] nodes = {"medication", "medicamentos", "Medicamentos", "comprimidos"};
fullPillsList.clear();
// 1. Tentar nos nós específicos
for (String node : nodes) {
rootRef.child(node).addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot snapshot) {
if (snapshot.exists()) {
parseSnapshot(snapshot, "Nó: " + node);
}
}
@Override public void onCancelled(@NonNull DatabaseError error) {}
});
}
// 2. Tentar também na raiz (caso os medicamentos estejam diretamente no topo)
rootRef.limitToFirst(50).addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot snapshot) {
if (snapshot.exists()) {
parseSnapshot(snapshot, "Raiz");
}
}
@Override public void onCancelled(@NonNull DatabaseError error) {}
});
}
private void parseSnapshot(DataSnapshot snapshot, String source) {
int count = 0;
for (DataSnapshot child : snapshot.getChildren()) {
String name = child.child("nome").getValue(String.class);
if (name == null) name = child.child("name").getValue(String.class);
if (name == null && !(child.getValue() instanceof java.util.Map)) {
// Se o valor for a própria string (ex: "Paracetamol")
name = child.getValue() instanceof String ? (String) child.getValue() : null;
}
if (name == null) name = child.getKey();
String dosage = child.child("dosagem").getValue(String.class);
if (dosage == null) dosage = child.child("dosage").getValue(String.class);
if (dosage == null) dosage = "";
if (name != null && !name.isEmpty()) {
boolean exists = false;
for (Comprimido p : fullPillsList) {
if (name.equals(p.nome)) { exists = true; break; }
}
if (!exists) {
fullPillsList.add(new Comprimido(name, dosage));
count++;
}
}
}
if (count > 0 && getContext() != null) {
Log.d("FirebaseSearch", "Carregados " + count + " de " + source);
// Toast.makeText(getContext(), "Fonte: " + source + " (" + count + ")", Toast.LENGTH_SHORT).show();
}
}
private void filterMedsLocally(String query) {
searchResults.clear();
if (query.isEmpty()) {
recyclerResults.setVisibility(View.GONE);
recyclerAdapter.notifyDataSetChanged();
return;
}
String lowerQuery = query.toLowerCase();
for (Comprimido p : fullPillsList) {
if (p.nome != null && p.nome.toLowerCase().contains(lowerQuery)) {
searchResults.add(p);
}
}
recyclerAdapter.notifyDataSetChanged();
recyclerResults.setVisibility(searchResults.isEmpty() ? View.GONE : View.VISIBLE);
}
private void handleError(DatabaseError error) {
Log.e("FirebaseSearch", "Erro: " + error.getMessage());
if (getContext() != null) {
Toast.makeText(getContext(), "Erro no Firebase: " + error.getMessage(), Toast.LENGTH_LONG).show();
}
}
}

View File

@@ -0,0 +1,141 @@
package com.example.cuida.ui.medication;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.example.cuida.data.model.Medication;
import com.example.cuida.databinding.FragmentMedicationBinding;
public class MedicationFragment extends Fragment {
private FragmentMedicationBinding binding;
private MedicationViewModel medicationViewModel;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
medicationViewModel = new ViewModelProvider(this).get(MedicationViewModel.class);
binding = FragmentMedicationBinding.inflate(inflater, container, false);
MedicationAdapter adapter = new MedicationAdapter(new MedicationAdapter.OnItemClickListener() {
@Override
public void onCheckClick(Medication medication) {
medicationViewModel.update(medication);
}
@Override
public void onItemClick(Medication medication) {
showMedicationDialog(medication);
}
});
binding.recyclerMedication.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerMedication.setAdapter(adapter);
medicationViewModel.getAllMedications().observe(getViewLifecycleOwner(), medications -> {
adapter.setMedications(medications);
if (medications != null && !medications.isEmpty()) {
binding.recyclerMedication.setVisibility(View.VISIBLE);
binding.textEmptyMedications.setVisibility(View.GONE);
} else {
binding.recyclerMedication.setVisibility(View.GONE);
binding.textEmptyMedications.setVisibility(View.VISIBLE);
}
});
binding.fabAddMedication.setOnClickListener(v -> showMedicationDialog(null));
return binding.getRoot();
}
private void showMedicationDialog(Medication medication) {
MedicationDialog dialog = new MedicationDialog();
dialog.setMedicationToEdit(medication);
dialog.setListener(medicationToSave -> {
// If it's an edit, cancel old alarms first
if (medication != null && medication.time != null) {
String[] oldTimes = medication.time.split(",\\s*");
for (String t : oldTimes) {
if (t.isEmpty()) continue;
try {
int oldId = (medication.name + t).hashCode();
com.example.cuida.utils.AlarmScheduler.cancelAlarm(requireContext(), oldId);
} catch (Exception e) {}
}
}
if (medication == null) {
medicationViewModel.insert(medicationToSave);
} else {
medicationViewModel.update(medicationToSave);
}
String[] times = medicationToSave.time.split(",\\s*");
for (String t : times) {
if (t.isEmpty()) continue;
try {
String[] timeParts = t.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 + ")";
int alarmId = (medicationToSave.name + t).hashCode();
com.example.cuida.utils.AlarmScheduler.scheduleAlarm(
requireContext(),
calendar.getTimeInMillis(),
title,
msg,
alarmId);
} catch (Exception e) {
e.printStackTrace();
}
}
dialog.dismiss();
});
dialog.setDeleteListener(medicationToDelete -> {
medicationViewModel.delete(medicationToDelete);
// Cancel all alarms for this medication
if (medicationToDelete.time != null) {
String[] times = medicationToDelete.time.split(",\\s*");
for (String t : times) {
if (t.isEmpty()) continue;
try {
int alarmId = (medicationToDelete.name + t).hashCode();
com.example.cuida.utils.AlarmScheduler.cancelAlarm(requireContext(), alarmId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
dialog.show(getParentFragmentManager(), "MedicationDialog");
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View File

@@ -0,0 +1,118 @@
package com.example.cuida.ui.medication;
import android.app.Application;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.example.cuida.data.model.Medication;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.Query;
import com.google.firebase.firestore.QueryDocumentSnapshot;
import java.util.ArrayList;
import java.util.List;
public class MedicationViewModel extends AndroidViewModel {
private final MutableLiveData<List<Medication>> allMedications = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Medication> nextMedication = new MutableLiveData<>(null);
private final FirebaseFirestore db;
private final FirebaseAuth auth;
public MedicationViewModel(@NonNull Application application) {
super(application);
db = FirebaseFirestore.getInstance();
auth = FirebaseAuth.getInstance();
fetchMedications();
}
private void fetchMedications() {
if (auth.getCurrentUser() == null)
return;
String userId = auth.getCurrentUser().getUid();
db.collection("medicamentos")
.whereEqualTo("userId", userId)
.addSnapshotListener((value, error) -> {
if (error != null) {
Log.e("MedicationViewModel", "Listen failed.", error);
return;
}
List<Medication> meds = new ArrayList<>();
if (value != null) {
for (QueryDocumentSnapshot doc : value) {
Medication med = doc.toObject(Medication.class);
med.setId(doc.getId()); // Ensure ID is set
meds.add(med);
}
}
// Sort locally to avoid needing a composite index in Firestore
meds.sort((m1, m2) -> {
if (m1.time == null && m2.time == null) return 0;
if (m1.time == null) return 1;
if (m2.time == null) return -1;
return m1.time.compareTo(m2.time);
});
allMedications.setValue(meds);
if (!meds.isEmpty()) {
nextMedication.setValue(meds.get(0));
} else {
nextMedication.setValue(null);
}
});
}
public LiveData<List<Medication>> getAllMedications() {
return allMedications;
}
public LiveData<Medication> getNextMedication() {
return nextMedication;
}
public void insert(Medication medication) {
if (auth.getCurrentUser() == null)
return;
String userId = auth.getCurrentUser().getUid();
medication.userId = userId;
db.collection("medicamentos")
.add(medication)
.addOnSuccessListener(documentReference -> Log.d("MedicationViewModel", "Medication added"))
.addOnFailureListener(e -> Log.w("MedicationViewModel", "Error adding medication", e));
}
public void update(Medication medication) {
if (auth.getCurrentUser() == null || medication.getId() == null)
return;
String userId = auth.getCurrentUser().getUid();
medication.userId = userId;
db.collection("medicamentos")
.document(medication.getId())
.set(medication)
.addOnSuccessListener(aVoid -> Log.d("MedicationViewModel", "Medication updated"))
.addOnFailureListener(e -> Log.w("MedicationViewModel", "Error updating medication", e));
}
public void delete(Medication medication) {
if (auth.getCurrentUser() == null || medication.getId() == null)
return;
db.collection("medicamentos")
.document(medication.getId())
.delete()
.addOnSuccessListener(aVoid -> Log.d("MedicationViewModel", "Medication deleted"))
.addOnFailureListener(e -> Log.w("MedicationViewModel", "Error deleting medication", e));
}
}

View File

@@ -0,0 +1,229 @@
package com.example.cuida.ui.profile;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import com.example.cuida.R;
import com.example.cuida.data.model.User;
import com.example.cuida.databinding.FragmentProfileBinding;
import com.example.cuida.ui.auth.LoginActivity;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.firestore.FirebaseFirestore;
public class ProfileFragment extends Fragment {
private FragmentProfileBinding binding;
private User currentUser;
private FirebaseFirestore db;
private FirebaseAuth auth;
private Uri tempProfileUri;
private ImageView dialogImageView;
private final androidx.activity.result.ActivityResultLauncher<String> pickMedia = registerForActivityResult(
new androidx.activity.result.contract.ActivityResultContracts.GetContent(), uri -> {
if (uri != null) {
tempProfileUri = uri;
try {
requireContext().getContentResolver().takePersistableUriPermission(uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION);
} catch (Exception e) {
Log.e("ProfileFragment", "Permission error: " + e.getMessage());
}
if (dialogImageView != null) {
dialogImageView.setImageURI(uri);
}
}
});
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
binding = FragmentProfileBinding.inflate(inflater, container, false);
db = FirebaseFirestore.getInstance();
auth = FirebaseAuth.getInstance();
loadUserData();
binding.buttonEditProfile.setOnClickListener(v -> showEditDialog());
binding.buttonLogout.setOnClickListener(v -> {
auth.signOut();
if (getContext() != null) {
requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE).edit().clear().apply();
}
startActivity(new Intent(getContext(), LoginActivity.class));
requireActivity().finish();
});
return binding.getRoot();
}
private void loadUserData() {
if (auth.getCurrentUser() == null) return;
String userId = auth.getCurrentUser().getUid();
if (currentUser == null) {
currentUser = new User();
currentUser.id = userId;
currentUser.email = auth.getCurrentUser().getEmail();
currentUser.name = auth.getCurrentUser().getDisplayName();
}
db.collection("utilizadores").document(userId).get()
.addOnSuccessListener(doc -> {
if (doc.exists() && isAdded()) {
currentUser.id = doc.getId();
String nome = doc.getString("nome_completo");
if (nome == null) nome = doc.getString("name");
currentUser.name = nome;
currentUser.email = doc.getString("email");
String utente = doc.getString("numero_utente");
if (utente == null) utente = doc.getString("utenteNumber");
currentUser.utenteNumber = utente;
currentUser.profilePictureUri = doc.getString("profilePictureUri");
Object ageObj = doc.get("idade");
if (ageObj == null) ageObj = doc.get("age");
if (ageObj instanceof Number) currentUser.age = ((Number) ageObj).intValue();
else if (ageObj instanceof String) {
try { currentUser.age = Integer.parseInt((String) ageObj); }
catch (Exception e) { currentUser.age = 0; }
}
updateUI();
}
});
}
private void updateUI() {
if (!isAdded() || binding == null || currentUser == null) return;
binding.profileName.setText(currentUser.name != null ? currentUser.name : "N/D");
binding.profileEmail.setText(currentUser.email != null ? currentUser.email : "N/D");
binding.profileAge.setText(currentUser.age > 0 ? String.valueOf(currentUser.age) : "N/D");
binding.profileUtente.setText(currentUser.utenteNumber != null ? currentUser.utenteNumber : "N/D");
if (currentUser.profilePictureUri != null && !currentUser.profilePictureUri.isEmpty()) {
ImageView profileImage = binding.getRoot().findViewById(R.id.profile_image);
if (profileImage != null) loadSafeImage(profileImage, currentUser.profilePictureUri);
}
}
private void loadSafeImage(ImageView view, String uriStr) {
if (view == null || uriStr == null) return;
try {
Uri uri = Uri.parse(uriStr);
if (uri.getScheme() != null && (uri.getScheme().equals("content") || uri.getScheme().equals("file"))) {
view.setImageURI(uri);
} else {
Log.d("ProfileFragment", "Skipping setImageURI for non-local scheme: " + uri.getScheme());
}
} catch (Exception e) {
Log.e("ProfileFragment", "Image load error: " + e.getMessage());
}
}
private void showEditDialog() {
if (currentUser == null) {
Toast.makeText(getContext(), "Dados não carregados.", Toast.LENGTH_SHORT).show();
return;
}
try {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
View dialogView = requireActivity().getLayoutInflater().inflate(R.layout.dialog_edit_profile, null);
builder.setView(dialogView);
AlertDialog dialog = builder.create();
EditText editName = dialogView.findViewById(R.id.edit_name);
EditText editAge = dialogView.findViewById(R.id.edit_age);
EditText editUtente = dialogView.findViewById(R.id.edit_utente);
EditText editEmail = dialogView.findViewById(R.id.edit_email);
dialogImageView = dialogView.findViewById(R.id.edit_profile_image);
if (editName == null || dialogImageView == null) return;
editName.setText(currentUser.name);
editAge.setText(String.valueOf(currentUser.age));
editUtente.setText(currentUser.utenteNumber);
editEmail.setText(currentUser.email);
if (currentUser.profilePictureUri != null) loadSafeImage(dialogImageView, currentUser.profilePictureUri);
dialogView.findViewById(R.id.button_change_photo).setOnClickListener(v -> pickMedia.launch("image/*"));
dialogImageView.setOnClickListener(v -> pickMedia.launch("image/*"));
dialogView.findViewById(R.id.button_change_password).setOnClickListener(v -> showChangePasswordDialog());
dialogView.findViewById(R.id.button_cancel).setOnClickListener(v -> dialog.dismiss());
dialogView.findViewById(R.id.button_save).setOnClickListener(v -> {
String newName = editName.getText().toString().trim();
String ageStr = editAge.getText().toString().trim();
String newUtente = editUtente.getText().toString().trim();
String newEmail = editEmail.getText().toString().trim();
if (newName.isEmpty() || ageStr.isEmpty() || newUtente.isEmpty() || newEmail.isEmpty()) {
Toast.makeText(getContext(), "Preencha todos os campos.", Toast.LENGTH_SHORT).show();
return;
}
currentUser.name = newName;
try { currentUser.age = Integer.parseInt(ageStr); } catch (Exception ignored) {}
currentUser.utenteNumber = newUtente;
if (tempProfileUri != null) currentUser.profilePictureUri = tempProfileUri.toString();
db.collection("utilizadores").document(currentUser.id).set(currentUser)
.addOnSuccessListener(aVoid -> {
Toast.makeText(getContext(), "Perfil atualizado!", Toast.LENGTH_SHORT).show();
loadUserData();
dialog.dismiss();
})
.addOnFailureListener(e -> Toast.makeText(getContext(), "Erro ao guardar.", Toast.LENGTH_SHORT).show());
});
dialog.show();
} catch (Exception e) {
Toast.makeText(getContext(), "Erro ao abrir edição.", Toast.LENGTH_SHORT).show();
}
}
private void showChangePasswordDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
View view = requireActivity().getLayoutInflater().inflate(R.layout.dialog_change_password, null);
builder.setView(view);
AlertDialog dialog = builder.create();
EditText editNewPassword = view.findViewById(R.id.new_password);
view.findViewById(R.id.button_save_password).setOnClickListener(v -> {
String newPass = editNewPassword.getText().toString();
if (newPass.length() < 6) {
Toast.makeText(getContext(), "Mínimo 6 caracteres.", Toast.LENGTH_SHORT).show();
return;
}
if (auth.getCurrentUser() != null) {
auth.getCurrentUser().updatePassword(newPass).addOnCompleteListener(task -> {
if (task.isSuccessful()) {
Toast.makeText(getContext(), "Sucesso!", Toast.LENGTH_SHORT).show();
dialog.dismiss();
} else {
Toast.makeText(getContext(), "Erro: " + task.getException().getMessage(), Toast.LENGTH_LONG).show();
}
});
}
});
view.findViewById(R.id.button_cancel_password).setOnClickListener(v -> dialog.dismiss());
dialog.show();
}
}

View File

@@ -0,0 +1,207 @@
package com.example.cuida.ui.schedule;
import android.app.AlertDialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.DatePicker;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.example.cuida.R;
import java.util.Calendar;
public class ScheduleAppointmentFragment extends Fragment {
private ScheduleViewModel scheduleViewModel;
private DatePicker datePicker;
private AutoCompleteTextView spinnerDoctor;
private RecyclerView recyclerTimeSlots;
private Button btnConfirm;
private TimeSlotAdapter timeSlotAdapter;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_schedule_appointment, container, false);
scheduleViewModel = new ViewModelProvider(this).get(ScheduleViewModel.class);
datePicker = root.findViewById(R.id.datePicker);
spinnerDoctor = root.findViewById(R.id.spinner_doctor);
recyclerTimeSlots = root.findViewById(R.id.recycler_time_slots);
btnConfirm = root.findViewById(R.id.btn_confirm_appointment);
setupDoctorSpinner();
setupDatePicker();
setupRecyclerView();
setupObservers();
btnConfirm.setOnClickListener(v -> {
com.google.android.material.textfield.TextInputEditText editReason = getView()
.findViewById(R.id.edit_reason);
String reason = editReason.getText().toString();
if (scheduleViewModel.getSelectedTime().getValue() == null) {
Toast.makeText(getContext(), "Selecione um horário.", Toast.LENGTH_SHORT).show();
return;
}
if (reason.isEmpty()) {
Toast.makeText(getContext(), "Indique o motivo.", Toast.LENGTH_SHORT).show();
return;
}
String selectedDoctor = spinnerDoctor.getText().toString();
if (selectedDoctor.isEmpty()) {
Toast.makeText(getContext(), "Selecione um médico.", Toast.LENGTH_SHORT).show();
return;
}
if (isUrgentSymptom(reason)) {
showUrgencyAlert(selectedDoctor, reason);
} else {
scheduleViewModel.confirmAppointment(selectedDoctor, reason);
}
});
return root;
}
private boolean isUrgentSymptom(String reason) {
String lowerReason = reason.toLowerCase();
String[] urgentKeywords = {
"dor no peito", "falta de ar", "desmaio", "sangramento",
"paralisia", "perda de vis", "dormência", "confusão",
"aperto no peito", "convulsão", "hemorragia", "asfixia"
};
for (String keyword : urgentKeywords) {
if (lowerReason.contains(keyword)) {
return true;
}
}
return false;
}
private void showUrgencyAlert(String selectedDoctor, String reason) {
new AlertDialog.Builder(requireContext())
.setTitle("Aviso de Urgência Médica")
.setMessage("O motivo indicado parece necessitar de atendimento urgente. Para situações graves, dirija-se ao Hospital mais próximo ou ligue 112.\n\nPretende continuar com o agendamento normal?")
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton("Agendar na mesma", (dialog, which) -> {
scheduleViewModel.confirmAppointment(selectedDoctor, reason);
})
.setNegativeButton("Cancelar", null)
.show();
}
private void setupDoctorSpinner() {
scheduleViewModel.getDoctorsList().observe(getViewLifecycleOwner(), doctors -> {
if (doctors != null) {
java.util.List<String> shuffledDoctors = new java.util.ArrayList<>(doctors);
java.util.Collections.shuffle(shuffledDoctors); // Randomize the names as requested
ArrayAdapter<String> adapter = new ArrayAdapter<>(requireContext(), android.R.layout.simple_dropdown_item_1line, shuffledDoctors);
spinnerDoctor.setAdapter(adapter);
}
});
spinnerDoctor.setOnItemClickListener((parent, view, position, id) -> {
String selectedDoctor = (String) parent.getItemAtPosition(position);
scheduleViewModel.setSelectedDoctor(selectedDoctor);
});
}
private void setupDatePicker() {
Calendar today = Calendar.getInstance();
datePicker.init(today.get(Calendar.YEAR), today.get(Calendar.MONTH),
today.get(Calendar.DAY_OF_MONTH), (view, year, monthOfYear, dayOfMonth) -> {
scheduleViewModel.setDate(year, monthOfYear, dayOfMonth);
});
// Set initial valid date in VM
scheduleViewModel.setDate(today.get(Calendar.YEAR), today.get(Calendar.MONTH),
today.get(Calendar.DAY_OF_MONTH));
// Prevent past dates
datePicker.setMinDate(System.currentTimeMillis() - 1000);
// Hide the year component
int yearSpinnerId = android.content.res.Resources.getSystem().getIdentifier("year", "id", "android");
if (yearSpinnerId != 0) {
View yearSpinner = datePicker.findViewById(yearSpinnerId);
if (yearSpinner != null) {
yearSpinner.setVisibility(View.GONE);
}
}
// Put day on left, month on right
int daySpinnerId = android.content.res.Resources.getSystem().getIdentifier("day", "id", "android");
int monthSpinnerId = android.content.res.Resources.getSystem().getIdentifier("month", "id", "android");
if (daySpinnerId != 0 && monthSpinnerId != 0) {
View daySpinner = datePicker.findViewById(daySpinnerId);
View monthSpinner = datePicker.findViewById(monthSpinnerId);
if (daySpinner != null && monthSpinner != null) {
ViewGroup parent = (ViewGroup) daySpinner.getParent();
if (parent != null && parent.equals(monthSpinner.getParent())) {
int dIndex = parent.indexOfChild(daySpinner);
int mIndex = parent.indexOfChild(monthSpinner);
// We want Day to be before Month (Day on Left, Month on Right)
if (dIndex > mIndex) {
parent.removeView(daySpinner);
parent.addView(daySpinner, mIndex);
}
}
}
}
}
private void setupRecyclerView() {
timeSlotAdapter = new TimeSlotAdapter();
timeSlotAdapter.setOnTimeSlotSelectedListener(time -> scheduleViewModel.setTime(time));
recyclerTimeSlots.setLayoutManager(new GridLayoutManager(getContext(), 4));
recyclerTimeSlots.setAdapter(timeSlotAdapter);
}
private void setupObservers() {
scheduleViewModel.getTimeSlots().observe(getViewLifecycleOwner(), slots -> {
timeSlotAdapter.setTimeSlots(slots);
});
scheduleViewModel.getSaveSuccess().observe(getViewLifecycleOwner(), success -> {
if (success) {
Toast.makeText(getContext(), "Consulta agendada!", Toast.LENGTH_SHORT).show();
NavController navController = Navigation.findNavController(getView());
navController.popBackStack();
}
});
scheduleViewModel.getSaveError().observe(getViewLifecycleOwner(), errorMsg -> {
if (errorMsg != null && !errorMsg.isEmpty()) {
new AlertDialog.Builder(requireContext())
.setTitle("Horário Indisponível")
.setMessage(errorMsg)
.setPositiveButton("OK", null)
.show();
}
});
scheduleViewModel.getSelectedDoctorSchedule().observe(getViewLifecycleOwner(), schedule -> {
android.widget.TextView textSchedule = getView().findViewById(R.id.text_doctor_schedule);
if (textSchedule != null) {
textSchedule.setText(schedule);
}
});
}
}

View File

@@ -0,0 +1,334 @@
package com.example.cuida.ui.schedule;
import android.app.Application;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.example.cuida.data.model.Appointment;
import com.example.cuida.utils.AlarmScheduler;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.QueryDocumentSnapshot;
import com.google.firebase.firestore.ListenerRegistration;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
public class ScheduleViewModel extends AndroidViewModel {
private final FirebaseFirestore db;
private final FirebaseAuth auth;
private final MutableLiveData<String> selectedDate = new MutableLiveData<>();
private final MutableLiveData<String> selectedTime = new MutableLiveData<>();
private final MutableLiveData<String> selectedDoctor = new MutableLiveData<>();
private final MutableLiveData<List<TimeSlot>> timeSlots = new MutableLiveData<>();
private final MutableLiveData<String> selectedDoctorSchedule = new MutableLiveData<>();
private final MutableLiveData<Boolean> saveSuccess = new MutableLiveData<>();
private final MutableLiveData<String> saveError = new MutableLiveData<>();
private final MutableLiveData<List<String>> doctorsList = new MutableLiveData<>(new ArrayList<>());
private final java.util.Map<String, String> doctorSchedules = new java.util.HashMap<>();
private ListenerRegistration snapshotListener;
public ScheduleViewModel(@NonNull Application application) {
super(application);
db = FirebaseFirestore.getInstance();
auth = FirebaseAuth.getInstance();
fetchDoctors();
}
private void fetchDoctors() {
db.collection("medicos")
.get()
.addOnCompleteListener(task -> {
if (task.isSuccessful() && task.getResult() != null) {
List<String> docs = new ArrayList<>();
doctorSchedules.clear();
for (QueryDocumentSnapshot document : task.getResult()) {
String name = document.getString("nome");
if (name == null) name = document.getString("nome_completo");
if (name == null) name = document.getString("name");
String specialty = document.getString("especialidade");
String gender = document.getString("genero");
String horario = document.getString("horario");
if (name != null && !name.trim().isEmpty()) {
String displayName = name;
if (specialty != null && !specialty.trim().isEmpty()) {
displayName += " - " + specialty;
}
if (!displayName.startsWith("Dr.") && !displayName.startsWith("Dra.")) {
if ("Feminino".equalsIgnoreCase(gender) || "Feminino".equals(gender)) {
displayName = "Dra. " + displayName;
} else {
displayName = "Dr. " + displayName;
}
}
docs.add(displayName);
if (horario != null) {
doctorSchedules.put(displayName, horario);
}
}
}
doctorsList.postValue(docs);
} else {
Log.e("ScheduleViewModel", "Error getting doctors", task.getException());
}
});
}
public void setDate(int year, int month, int dayOfMonth) {
String date = String.format("%02d/%02d/%04d", dayOfMonth, month + 1, year);
selectedDate.setValue(date);
loadTimeSlots(date);
}
public void setSelectedDoctor(String doctor) {
selectedDoctor.setValue(doctor);
String schedule = doctorSchedules.get(doctor);
selectedDoctorSchedule.setValue(schedule != null ? "Horário: " + schedule : "Horário: 08:00 - 19:00");
String date = selectedDate.getValue();
if (date != null) {
loadTimeSlots(date);
}
}
public LiveData<String> getSelectedDoctorSchedule() {
return selectedDoctorSchedule;
}
public LiveData<String> getSelectedDate() {
return selectedDate;
}
public void setTime(String time) {
selectedTime.setValue(time);
List<TimeSlot> currentSlots = timeSlots.getValue();
if (currentSlots != null) {
for (TimeSlot slot : currentSlots) {
slot.setSelected(slot.getTime().equals(time));
}
timeSlots.setValue(currentSlots);
}
}
public LiveData<String> getSelectedTime() {
return selectedTime;
}
public LiveData<List<TimeSlot>> getTimeSlots() {
return timeSlots;
}
public LiveData<Boolean> getSaveSuccess() {
return saveSuccess;
}
public LiveData<String> getSaveError() {
return saveError;
}
public LiveData<List<String>> getDoctorsList() {
return doctorsList;
}
private void loadTimeSlots(String date) {
if (snapshotListener != null) {
snapshotListener.remove();
snapshotListener = null;
}
// Init slots immediately to prevent "disappearing" hours while waiting for network.
timeSlots.setValue(generateTimeSlots(new ArrayList<>(), date));
if (auth.getCurrentUser() == null) return;
String userId = auth.getCurrentUser().getUid();
String doctor = selectedDoctor.getValue();
// Listen in REAL-TIME for all appointments on the selected date
snapshotListener = db.collection("consultas")
.whereEqualTo("date", date)
.addSnapshotListener((queryDocumentSnapshots, e) -> {
if (e != null) {
Log.e("ScheduleViewModel", "Listen failed.", e);
return;
}
List<String> bookedTimes = new ArrayList<>();
if (queryDocumentSnapshots != null) {
for (QueryDocumentSnapshot document : queryDocumentSnapshots) {
Appointment appt = document.toObject(Appointment.class);
boolean isDoctorAppointment = doctor != null && doctor.equals(appt.type);
if (isDoctorAppointment && appt.time != null) {
if (!bookedTimes.contains(appt.time)) {
bookedTimes.add(appt.time);
}
}
}
}
List<TimeSlot> slots = generateTimeSlots(bookedTimes, date);
timeSlots.setValue(slots);
});
}
@Override
protected void onCleared() {
super.onCleared();
if (snapshotListener != null) {
snapshotListener.remove();
}
}
private List<TimeSlot> generateTimeSlots(List<String> bookedTimes, String selectedDateStr) {
List<TimeSlot> slots = new ArrayList<>();
int startHour = 8;
int endHour = 19;
int startMinute = 0;
int endMinute = 0;
String doctor = selectedDoctor.getValue();
String schedule = doctorSchedules.get(doctor);
if (schedule != null && schedule.contains(" - ")) {
try {
String[] parts = schedule.split(" - ");
String[] startParts = parts[0].split(":");
String[] endParts = parts[1].split(":");
startHour = Integer.parseInt(startParts[0]);
startMinute = Integer.parseInt(startParts[1]);
endHour = Integer.parseInt(endParts[0]);
endMinute = Integer.parseInt(endParts[1]);
} catch (Exception e) {
Log.e("ScheduleViewModel", "Error parsing schedule: " + schedule);
}
}
Calendar now = Calendar.getInstance();
boolean isToday = false;
if (selectedDateStr != null) {
String todayStr = String.format("%02d/%02d/%04d",
now.get(Calendar.DAY_OF_MONTH),
now.get(Calendar.MONTH) + 1,
now.get(Calendar.YEAR));
if (todayStr.equals(selectedDateStr)) {
isToday = true;
}
}
int currentHour = now.get(Calendar.HOUR_OF_DAY);
int currentMinute = now.get(Calendar.MINUTE);
Calendar cursor = Calendar.getInstance();
cursor.set(Calendar.HOUR_OF_DAY, startHour);
cursor.set(Calendar.MINUTE, startMinute);
cursor.set(Calendar.SECOND, 0);
cursor.set(Calendar.MILLISECOND, 0);
Calendar endLimit = Calendar.getInstance();
endLimit.set(Calendar.HOUR_OF_DAY, endHour);
endLimit.set(Calendar.MINUTE, endMinute);
endLimit.set(Calendar.SECOND, 0);
endLimit.set(Calendar.MILLISECOND, 0);
while (cursor.before(endLimit)) {
int h = cursor.get(Calendar.HOUR_OF_DAY);
int m = cursor.get(Calendar.MINUTE);
String timeStr = String.format("%02d:%02d", h, m);
if (!isToday || h > currentHour || (h == currentHour && m > currentMinute)) {
addSlot(slots, timeStr, bookedTimes);
}
cursor.add(Calendar.MINUTE, 20);
}
return slots;
}
private void addSlot(List<TimeSlot> slots, String time, List<String> bookedTimes) {
boolean isBooked = bookedTimes.contains(time);
boolean isSelected = time.equals(selectedTime.getValue());
slots.add(new TimeSlot(time, isBooked, isSelected));
}
public void confirmAppointment(String type, String reason) {
String date = selectedDate.getValue();
String time = selectedTime.getValue();
if (auth.getCurrentUser() == null)
return;
String userId = auth.getCurrentUser().getUid();
if (date != null && time != null) {
saveError.setValue(null); // Resetar erro antes de validar
// Validar no servidor se o horário já está ocupado por este médico
db.collection("consultas")
.whereEqualTo("type", type)
.whereEqualTo("date", date)
.whereEqualTo("time", time)
.get()
.addOnSuccessListener(queryDocumentSnapshots -> {
if (!queryDocumentSnapshots.isEmpty()) {
// Já existe uma consulta!
saveError.postValue("Este horário já foi marcado por outro paciente. Por favor, escolha outro.");
} else {
// O horário está livre, prosseguir com a marcação
Appointment appointment = new Appointment(type, date, time, reason, false, userId, "Pendente");
db.collection("consultas")
.add(appointment)
.addOnSuccessListener(documentReference -> {
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]);
Calendar baseCal = Calendar.getInstance();
baseCal.set(year, month, day, hour, minute, 0);
// Schedule 24 hours before
Calendar cal24h = (Calendar) baseCal.clone();
cal24h.add(Calendar.DAY_OF_YEAR, -1);
if (cal24h.getTimeInMillis() > System.currentTimeMillis()) {
AlarmScheduler.scheduleAlarm(getApplication(), cal24h.getTimeInMillis(),
"Lembrete de Consulta", "A sua consulta é amanhã às " + time,
(date + time + "24h").hashCode());
}
// Schedule 30 minutes before
Calendar cal30m = (Calendar) baseCal.clone();
cal30m.add(Calendar.MINUTE, -30);
if (cal30m.getTimeInMillis() > System.currentTimeMillis()) {
AlarmScheduler.scheduleAlarm(getApplication(), cal30m.getTimeInMillis(),
"Lembrete de Consulta", "A sua consulta é daqui a 30 minutos (" + time + ")",
(date + time + "30m").hashCode());
}
} catch (Exception e) {
e.printStackTrace();
}
saveSuccess.postValue(true);
})
.addOnFailureListener(e -> Log.e("ScheduleViewModel", "Failed to confirm appt", e));
}
}).addOnFailureListener(e -> {
saveError.postValue("Erro ao verificar a disponibilidade do horário. Tente novamente.");
});
}
}
}

View File

@@ -0,0 +1,52 @@
package com.example.cuida.ui.schedule;
import java.util.Objects;
public class TimeSlot {
private String time;
private boolean isBooked;
private boolean isSelected;
public TimeSlot(String time, boolean isBooked, boolean isSelected) {
this.time = time;
this.isBooked = isBooked;
this.isSelected = isSelected;
}
public String getTime() {
return time;
}
public boolean isBooked() {
return isBooked;
}
public void setBooked(boolean booked) {
isBooked = booked;
}
public boolean isSelected() {
return isSelected;
}
public void setSelected(boolean selected) {
isSelected = selected;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
TimeSlot timeSlot = (TimeSlot) o;
return isBooked == timeSlot.isBooked &&
isSelected == timeSlot.isSelected &&
Objects.equals(time, timeSlot.time);
}
@Override
public int hashCode() {
return Objects.hash(time, isBooked, isSelected);
}
}

View File

@@ -0,0 +1,75 @@
package com.example.cuida.ui.schedule;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.example.cuida.R;
import com.google.android.material.button.MaterialButton;
import java.util.ArrayList;
import java.util.List;
public class TimeSlotAdapter extends RecyclerView.Adapter<TimeSlotAdapter.ViewHolder> {
private List<TimeSlot> timeSlots = new ArrayList<>();
private OnTimeSlotSelectedListener listener;
public interface OnTimeSlotSelectedListener {
void onTimeSlotSelected(String time);
}
public void setOnTimeSlotSelectedListener(OnTimeSlotSelectedListener listener) {
this.listener = listener;
}
public void setTimeSlots(List<TimeSlot> slots) {
this.timeSlots = slots;
notifyDataSetChanged();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_time_slot, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
TimeSlot slot = timeSlots.get(position);
holder.btnTimeSlot.setText(slot.getTime());
if (slot.isBooked()) {
holder.btnTimeSlot.setEnabled(false);
holder.btnTimeSlot.setAlpha(0.5f);
holder.btnTimeSlot.setChecked(false);
holder.btnTimeSlot.setOnClickListener(null);
} else {
holder.btnTimeSlot.setEnabled(true);
holder.btnTimeSlot.setAlpha(1.0f);
holder.btnTimeSlot.setChecked(slot.isSelected());
holder.btnTimeSlot.setOnClickListener(v -> {
if (listener != null) {
listener.onTimeSlotSelected(slot.getTime());
}
});
}
}
@Override
public int getItemCount() {
return timeSlots.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
MaterialButton btnTimeSlot;
ViewHolder(View itemView) {
super(itemView);
btnTimeSlot = itemView.findViewById(R.id.btn_time_slot);
}
}
}

View File

@@ -0,0 +1,133 @@
package com.example.cuida.ui.sns24;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import com.example.cuida.databinding.FragmentSns24Binding;
import com.example.cuida.services.Gemini;
public class Sns24Fragment extends Fragment {
private FragmentSns24Binding binding;
private Gemini gemini;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
binding = FragmentSns24Binding.inflate(inflater, container, false);
View root = binding.getRoot();
gemini = new Gemini();
// 1. Botão de Chamada SNS 24
binding.buttonCallSns.setOnClickListener(v -> {
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:808242424"));
startActivity(intent);
});
// 2. Esconder o botão de hospital inicialmente
binding.buttonFindHospital.setVisibility(View.GONE);
// 3. Botão Triagem IA
binding.buttonAiTriage.setOnClickListener(v -> {
String symptoms = binding.inputSymptoms.getText().toString().trim();
if (!symptoms.isEmpty()) {
hideKeyboard();
analyzeSymptomsWithGemini(symptoms);
} else {
Toast.makeText(getContext(), "Descreva os sintomas.", Toast.LENGTH_SHORT).show();
}
});
return root;
}
private void hideKeyboard() {
View view = getActivity() != null ? getActivity().getCurrentFocus() : null;
if (view != null) {
android.view.inputmethod.InputMethodManager imm = (android.view.inputmethod.InputMethodManager)
getActivity().getSystemService(android.content.Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
}
}
private void analyzeSymptomsWithGemini(String symptoms) {
binding.buttonAiTriage.setEnabled(false);
binding.textAiResult.setVisibility(View.VISIBLE);
binding.textAiResult.setText("A analisar sintomas...");
binding.buttonFindHospital.setVisibility(View.GONE);
String prompt = "Atua como triagem médica de urgência (estilo SNS 24). " +
"Sê extremamente direto, objetivo e conciso. Não uses introduções ou saudações. " +
"Responde apenas com: 1) Causa provável, 2) Ação imediata recomendada. " +
"Se os sintomas indicarem perigo de vida ou necessidade de observação urgente, OBRIGATORIAMENTE começa a tua primeira linha com a palavra [GRAVE]. " +
"Sintomas do paciente: " + symptoms;
gemini.fazerPergunta(prompt, new Gemini.GeminiCallback() {
@Override
public void onSuccess(String result) {
if (getActivity() != null && binding != null) {
getActivity().runOnUiThread(() -> {
String displayResult = result.replace("[GRAVE]", "").trim();
binding.textAiResult.setText(displayResult);
binding.buttonAiTriage.setEnabled(true);
if (result.contains("[GRAVE]")) {
binding.buttonFindHospital.setVisibility(View.VISIBLE);
binding.buttonFindHospital.setOnClickListener(v -> {
Uri gmmIntentUri = Uri.parse("geo:0,0?q=hospital+mais+proximo");
Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);
mapIntent.setPackage("com.google.android.apps.maps");
startActivity(mapIntent);
});
}
saveTriageToHistory(symptoms, displayResult);
});
}
}
@Override
public void onError(Throwable t) {
if (getActivity() != null && binding != null) {
getActivity().runOnUiThread(() -> {
binding.textAiResult.setText("Erro na ligação: " + t.getMessage());
binding.buttonAiTriage.setEnabled(true);
});
}
}
});
}
private void saveTriageToHistory(String symptoms, String result) {
if (getActivity() == null) return;
com.google.firebase.auth.FirebaseUser user = com.google.firebase.auth.FirebaseAuth.getInstance().getCurrentUser();
if (user == null) return;
java.util.Map<String, Object> triage = new java.util.HashMap<>();
triage.put("userId", user.getUid());
triage.put("sintomas", symptoms);
triage.put("resultado", result);
triage.put("data", new java.text.SimpleDateFormat("dd/MM/yyyy HH:mm", java.util.Locale.getDefault()).format(new java.util.Date()));
triage.put("timestamp", com.google.firebase.firestore.FieldValue.serverTimestamp());
com.google.firebase.firestore.FirebaseFirestore.getInstance()
.collection("triagens").add(triage);
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View File

@@ -0,0 +1,64 @@
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);
}
}
public static void cancelAlarm(Context context, int requestCode) {
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (alarmManager == null)
return;
Intent intent = new Intent(context, AlarmReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(
context,
requestCode,
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
alarmManager.cancel(pendingIntent);
pendingIntent.cancel();
}
}

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

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@android:color/white" />
<foreground android:drawable="@drawable/ic_logo_scaled" />
</adaptive-icon>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:startColor="@color/primary_color"
android:endColor="@color/secondary_color"
android:angle="135"
android:type="linear" />
<corners
android:bottomLeftRadius="36dp"
android:bottomRightRadius="36dp" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FFFFFF" />
<stroke android:width="1dp" android:color="#DDDDDD" />
<corners android:radius="8dp" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke android:width="1dp" android:color="#D32F2F" />
<corners android:radius="28dp" />
<solid android:color="@android:color/transparent" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke android:width="1dp" android:color="#2196F3" />
<corners android:radius="28dp" />
<solid android:color="@android:color/transparent" />
</shape>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#005B96"
android:pathData="M0,0h108v108h-108z"/>
<path
android:fillColor="#FFFFFF"
android:pathData="M48,30h12v24h24v12h-24v24h-12v-24h-24v-12h24z"/>
</vector>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_logo" />
</layer-list>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#005B96"
android:pathData="M54,54m-54,0a54,54 0 1,1 108,0a54,54 0 1,1 -108,0"/>
<path
android:fillColor="#FFFFFF"
android:pathData="M48,30h12v24h24v12h-24v24h-12v-24h-24v-12h24z"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:top="-35dp"
android:bottom="-35dp"
android:left="-35dp"
android:right="-35dp">
<bitmap
android:src="@drawable/ic_logo"
android:gravity="center" />
</item>
</layer-list>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="24dp"
android:background="@color/background_color">
<ImageView
android:layout_width="187dp"
android:layout_height="177dp"
android:layout_marginBottom="24dp"
android:scaleType="centerCrop"
android:src="@drawable/ic_logo" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/forgot_password"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="@color/primary_color"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Insira o seu email para recuperar a palavra-passe."
android:gravity="center"
android:textSize="16sp"
android:layout_marginBottom="32dp"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/email_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/email_hint"
android:inputType="textEmailAddress"
android:imeOptions="actionDone" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/reset_button"
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="Recuperar Palavra-passe"
android:textSize="16sp"
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/back_to_login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Voltar ao Login"
android:textColor="@color/primary_color"
android:textStyle="bold"/>
</LinearLayout>

View File

@@ -0,0 +1,141 @@
<?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="match_parent"
android:fillViewport="true"
android:background="@color/background_color">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:padding="24dp">
<ImageView
android:layout_width="161dp"
android:layout_height="160dp"
android:layout_marginBottom="32dp"
android:contentDescription="@string/app_name"
android:src="@drawable/ic_logo" />
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="@color/surface_color">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/login_title"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="@color/primary_color"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="24dp"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/email_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/email_hint"
android:autofillHints="emailAddress"
android:inputType="textEmailAddress"
android:imeOptions="actionNext"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/password_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/password_hint"
android:autofillHints="password"
android:inputType="textPassword"
android:imeOptions="actionDone"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/checkbox_remember_me"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Lembrar-me"
android:layout_gravity="start"
android:layout_marginBottom="16dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/login_button"
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="@string/login_button"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/biometric_button"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Login com Biometria"
android:icon="@android:drawable/ic_dialog_info"
android:layout_marginBottom="16dp"
android:visibility="gone"/>
<TextView
android:id="@+id/forgot_password_link"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/forgot_password"
android:textColor="@color/secondary_color"
android:layout_gravity="center_horizontal"
android:padding="8dp"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no_account"
android:textColor="@color/text_secondary"
android:layout_marginEnd="8dp"/>
<TextView
android:id="@+id/register_link"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/register_title"
android:textStyle="bold"
android:textColor="@color/secondary_color"/>
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/mobile_navigation" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/bottom_nav_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,177 @@
<?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="match_parent"
android:fillViewport="true"
android:background="@color/background_color">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:padding="24dp">
<ImageView
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginBottom="24dp"
android:contentDescription="@string/app_name"
android:src="@drawable/ic_logo" />
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardElevation="4dp"
app:cardBackgroundColor="@color/surface_color">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/register_title"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="@color/primary_color"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="24dp"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/name_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/name_hint"
android:inputType="textPersonName" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginEnd="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/age_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/age_hint"
android:maxLength="3"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="0dp"
android:layout_weight="2"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/utente_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Nº Utente"
android:maxLength="9"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:hint="Sexo">
<AutoCompleteTextView
android:id="@+id/gender_auto_complete"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/email_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/email_hint"
android:autofillHints="emailAddress"
android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/password_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/password_hint"
android:autofillHints="password"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/register_button"
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="@string/register_button"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="center_horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/already_account"
android:textColor="@color/text_secondary"
android:layout_marginEnd="8dp"/>
<TextView
android:id="@+id/login_link"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/login_title"
android:textStyle="bold"
android:textColor="@color/secondary_color"/>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="24dp"
android:background="@color/background_color">
<ImageView
android:layout_width="187dp"
android:layout_height="177dp"
android:layout_marginBottom="24dp"
android:scaleType="centerCrop"
android:src="@drawable/ic_logo" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Nova Palavra-passe"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="@color/primary_color"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Insira e confirme a sua nova palavra-passe."
android:gravity="center"
android:textSize="16sp"
android:layout_marginBottom="32dp"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/new_password_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Nova palavra-passe"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/confirm_new_password_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Confirmar palavra-passe"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/save_new_password_button"
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="Guardar Palavra-passe"
android:textSize="16sp"
android:layout_marginBottom="16dp"/>
</LinearLayout>

View File

@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/layout_med_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:hint="Nome do Medicamento"
android:layout_marginBottom="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_med_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapWords"
android:imeOptions="actionNext"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Search Results RecyclerView -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_search_results"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="-8dp"
android:layout_marginBottom="16dp"
android:visibility="gone"
android:background="#FFFFFF"
android:elevation="4dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="4dp">
<TextView
android:text="Horários"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="@color/black"
android:textSize="14sp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_add_time"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Adicionar"
android:padding="0dp"
android:minWidth="0dp"
android:minHeight="0dp"/>
</LinearLayout>
<com.google.android.material.chip.ChipGroup
android:id="@+id/chip_group_times"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:padding="4dp"/>
<TextView
android:text="Via de Administração"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="@color/black"
android:layout_marginBottom="4dp"/>
<RadioGroup
android:id="@+id/radio_group_route"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<RadioButton
android:id="@+id/radio_oral"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Via Oral (Pela boca)" />
<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 (Pela nariz/boca)" />
</RadioGroup>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:hint="Notas (Opcional)"
android:layout_marginBottom="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_med_notes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:imeOptions="actionDone"
android:minLines="2"
android:maxLines="3" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Alterar Palavra-passe"
android:textSize="20sp"
android:textStyle="bold"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="24dp"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/new_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Nova Palavra-passe"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<com.google.android.material.button.MaterialButton
android:id="@+id/button_cancel_password"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cancelar"
android:layout_marginEnd="8dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_save_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Salvar" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,128 @@
<?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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<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="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"
android:text="Editar Dados"
android:textSize="20sp"
android:textStyle="bold"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="24dp"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Nome"
android:inputType="textPersonName" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_age"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Idade"
android:maxLength="3"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_utente"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Nº Utente"
android:maxLength="9"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Email"
android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_change_password"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Alterar Palavra-passe"
android:layout_marginBottom="24dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<com.google.android.material.button.MaterialButton
android:id="@+id/button_cancel"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cancelar"
android:layout_marginEnd="8dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Salvar" />
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_appointments"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="@color/primary_color"
android:layout_marginBottom="16dp"/>
<!-- Placeholder for RecyclerView -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Futuras"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_appointments_future"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Passadas"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_appointments_past"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</FrameLayout>

View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
android:padding="16dp">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image_profile_home"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_placeholder"
android:scaleType="centerCrop"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.CornerSize50Percent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:id="@+id/text_greeting"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="Olá, utilizador!"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="@color/primary_color"
app:layout_constraintTop_toTopOf="@id/image_profile_home"
app:layout_constraintBottom_toBottomOf="@id/image_profile_home"
app:layout_constraintStart_toEndOf="@id/image_profile_home"/>
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_next_medication"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
app:cardCornerRadius="16dp"
app:cardElevation="4dp"
app:layout_constraintTop_toBottomOf="@id/image_profile_home">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Próxima Medicação"
android:textStyle="bold"
android:textSize="18sp"
android:textColor="@color/primary_color"/>
<TextView
android:id="@+id/next_med_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Paracetamol 500mg"
android:textSize="16sp"
android:layout_marginTop="8dp"/>
<TextView
android:id="@+id/next_med_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hoje, 14:00"
android:textSize="14sp"
android:textColor="@android:color/darker_gray"
android:layout_marginTop="4dp"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_book_appointment"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="Marcar Consulta"
android:textSize="16sp"
android:layout_marginTop="24dp"
app:icon="@android:drawable/ic_menu_my_calendar"
app:iconGravity="textStart"
app:layout_constraintTop_toBottomOf="@id/card_next_medication"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
android:padding="16dp">
<TextView
android:id="@+id/text_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_medication"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="@color/primary_color"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginBottom="16dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_medication"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/text_title"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/text_empty_medications"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Ainda não tem medicamentos guardados."
android:textSize="16sp"
android:textColor="@color/text_secondary"
android:gravity="center"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/text_title"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_add_medication"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@android:drawable/ic_input_add"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:contentDescription="Adicionar Medicamento"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="match_parent"
android:orientation="vertical"
android:padding="24dp"
android:gravity="center_horizontal"
android:background="@color/background_color">
<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
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardElevation="8dp"
app:cardCornerRadius="24dp"
app:cardBackgroundColor="@color/surface_color"
android:layout_marginBottom="32dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp"
android:gravity="center">
<TextView
android:id="@+id/profile_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Nome do Utilizador"
android:textSize="22sp"
android:textStyle="bold"
android:textColor="@color/text_primary"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/profile_email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="email@exemplo.com"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:layout_marginBottom="24dp"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#E0E0E0"
android:layout_marginBottom="16dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_marginBottom="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Idade: "
android:textSize="16sp"
android:textColor="@color/text_secondary"/>
<TextView
android:id="@+id/profile_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="--"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/text_primary"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Nº Utente: "
android:textSize="16sp"
android:textColor="@color/text_secondary"/>
<TextView
android:id="@+id/profile_utente"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="--"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/text_primary"/>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_edit_profile"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="Editar Dados"
android:layout_marginBottom="16dp"
android:backgroundTint="@color/secondary_color"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_logout"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="Terminar Sessão"
app:strokeColor="@color/error_color"
android:textColor="@color/error_color"/>
</LinearLayout>

View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Agendar Consulta"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="@color/black"
android:layout_marginBottom="24dp"/>
<!-- 1. Selecionar Data -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Selecionar Data"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<DatePicker
android:id="@+id/datePicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:calendarViewShown="false"
android:datePickerMode="spinner"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="16dp"/>
<!-- 2. Selecionar Médico/Especialidade -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Selecionar Médico/Especialidade"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:hint="Escolha o médico">
<AutoCompleteTextView
android:id="@+id/spinner_doctor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/text_doctor_schedule"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Horário: --"
android:textSize="14sp"
android:textColor="@color/black"
android:layout_marginBottom="16dp"
android:textStyle="italic"/>
<!-- 3. Selecionar Horário -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Selecionar Horário"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_time_slots"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginBottom="16dp"/>
<!-- 4. Motivo -->
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_reason"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Motivo da Consulta"
android:inputType="text"
android:imeOptions="actionDone"
android:minLines="2"
android:maxLines="4"/>
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btn_confirm_appointment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Confirmar Agendamento"/>
</LinearLayout>

View File

@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="match_parent"
android:orientation="vertical"
android:padding="16dp"
android:gravity="center_horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_sns24"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="@color/primary_color"
android:layout_marginBottom="32dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_call_sns"
android:layout_width="match_parent"
android:layout_height="80dp"
android:text="Ligar SNS 24 (808 24 24 24)"
android:textSize="18sp"
android:backgroundTint="@color/teal_700"
app:icon="@android:drawable/ic_menu_call"
app:iconGravity="textStart"
android:layout_marginBottom="32dp"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#CCCCCC"
android:layout_marginBottom="24dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Triagem com Inteligência Artificial"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/primary_color"
android:layout_marginBottom="16dp"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Descreva o que está a sentir..."
android:layout_marginBottom="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_symptoms"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapSentences"
android:imeOptions="actionDone"
android:minLines="3"
android:maxLines="5"
android:gravity="top|start"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_ai_triage"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="Analisar Sintomas"
android:textSize="16sp"
app:cornerRadius="8dp"
android:layout_marginBottom="24dp"/>
<TextView
android:id="@+id/text_ai_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@android:color/black"
android:padding="16dp"
android:background="#F5F5F5"
android:visibility="gone"
android:layout_marginBottom="16dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_find_hospital"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="Hospital mais próximo"
android:textSize="16sp"
android:backgroundTint="@android:color/holo_red_dark"
app:icon="@android:drawable/ic_menu_mapmode"
app:cornerRadius="8dp"
android:visibility="gone"/>
</LinearLayout>

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 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"
android:layout_margin="8dp"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
app:cardCornerRadius="16dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/text_type"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Tipo de Consulta"
android:textStyle="bold"
android:textSize="18sp"
android:textColor="@color/primary_color"/>
<TextView
android:id="@+id/text_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pendente"
android:textStyle="bold"
android:textSize="14sp"
android:padding="4dp"
android:background="#FFE0B2"
android:textColor="#F57C00"
android:gravity="end"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp">
<TextView
android:id="@+id/text_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="dd/MM/yyyy"
android:layout_marginEnd="16dp"/>
<TextView
android:id="@+id/text_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="HH:mm"
android:textStyle="italic"/>
</LinearLayout>
<TextView
android:id="@+id/text_reason"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Motivo: --"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:layout_marginTop="8dp"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:id="@+id/text_pill_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/primary_color" />
</LinearLayout>

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 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"
android:layout_margin="8dp"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
app:cardCornerRadius="16dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical">
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@android:drawable/ic_menu_agenda"
app:tint="@color/secondary_color"
android:layout_marginEnd="16dp"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_weight="1">
<TextView
android:id="@+id/text_med_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Nome Medicamento"
android:textStyle="bold"
android:textSize="18sp"
android:textColor="@color/primary_color"/>
<TextView
android:id="@+id/text_med_dosage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Dosagem"
android:layout_marginTop="4dp"/>
<TextView
android:id="@+id/text_med_notes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Observações"
android:textStyle="italic"
android:textSize="12sp"
android:layout_marginTop="2dp"/>
<TextView
android:id="@+id/text_med_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="HH:mm"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/black"
android:layout_marginTop="6dp"/>
</LinearLayout>
<CheckBox
android:id="@+id/checkbox_taken"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:buttonTint="@color/primary_color"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/btn_time_slot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="09:00"
android:checkable="true"
android:layout_margin="4dp"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_home"
android:icon="@android:drawable/ic_menu_today"
android:title="@string/title_home" />
<item
android:id="@+id/navigation_appointments"
android:icon="@android:drawable/ic_menu_my_calendar"
android:title="@string/title_appointments" />
<item
android:id="@+id/navigation_medication"
android:icon="@android:drawable/ic_menu_agenda"
android:title="@string/title_medication" />
<item
android:id="@+id/navigation_sns24"
android:icon="@android:drawable/ic_menu_call"
android:title="@string/title_sns24" />
<item
android:id="@+id/navigation_profile"
android:icon="@android:drawable/ic_menu_myplaces"
android:title="@string/title_profile" />
</menu>

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mobile_navigation"
app:startDestination="@+id/navigation_home">
<fragment
android:id="@+id/navigation_home"
android:name="com.example.cuida.ui.home.HomeFragment"
android:label="@string/title_home"
tools:layout="@layout/fragment_home">
<action
android:id="@+id/action_home_to_schedule_appointment"
app:destination="@id/navigation_schedule_appointment" />
</fragment>
<fragment
android:id="@+id/navigation_appointments"
android:name="com.example.cuida.ui.appointments.AppointmentsFragment"
android:label="@string/title_appointments"
tools:layout="@layout/fragment_appointments">
<action
android:id="@+id/action_appointments_to_schedule"
app:destination="@id/navigation_schedule_appointment" />
</fragment>
<fragment
android:id="@+id/navigation_medication"
android:name="com.example.cuida.ui.medication.MedicationFragment"
android:label="@string/title_medication"
tools:layout="@layout/fragment_medication" />
<fragment
android:id="@+id/navigation_sns24"
android:name="com.example.cuida.ui.sns24.Sns24Fragment"
android:label="@string/title_sns24"
tools:layout="@layout/fragment_sns24" />
<fragment
android:id="@+id/navigation_profile"
android:name="com.example.cuida.ui.profile.ProfileFragment"
android:label="@string/title_profile"
tools:layout="@layout/fragment_profile" />
<fragment
android:id="@+id/navigation_schedule_appointment"
android:name="com.example.cuida.ui.schedule.ScheduleAppointmentFragment"
android:label="Agendar Consulta"
tools:layout="@layout/fragment_schedule_appointment" />
</navigation>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- App Brand Colors -->
<color name="primary_color">#0066CC</color> <!-- Modern Medical Blue -->
<color name="primary_light_color">#E3F2FD</color>
<color name="primary_dark_color">#004C99</color>
<color name="secondary_color">#000000</color> <!-- Black -->
<color name="secondary_dark_color">#000000</color>
<color name="background_color">#F8F9FA</color> <!-- Soft White -->
<color name="surface_color">#FFFFFF</color>
<color name="text_primary">#202124</color> <!-- Softer Black -->
<color name="text_secondary">#5F6368</color> <!-- Grey text -->
<color name="error_color">#B00020</color>
<!-- Standard overrides -->
<color name="purple_200">#90CAF9</color>
<color name="purple_500">#0066CC</color>
<color name="purple_700">#004C99</color>
<color name="teal_200">#69F0AE</color>
<color name="teal_700">#000000</color>
<color name="black">#000000</color>
<color name="white">#FFFFFF</color>
</resources>

View File

@@ -0,0 +1,21 @@
<resources>
<string name="app_name">Cuida</string>
<string name="title_home">Início</string>
<string name="title_appointments">Consultas</string>
<string name="title_medication">Medicação</string>
<string name="title_sns24">SNS 24</string>
<string name="title_profile">Perfil</string>
<!-- Auth -->
<string name="login_title">Iniciar Sessão</string>
<string name="register_title">Criar Conta</string>
<string name="forgot_password">Esqueci-me da Palavra-passe</string>
<string name="email_hint">Email</string>
<string name="password_hint">Palavra-passe</string>
<string name="name_hint">Nome Completo</string>
<string name="age_hint">Idade</string>
<string name="login_button">Entrar</string>
<string name="register_button">Registar</string>
<string name="no_account">Não tem conta?</string>
<string name="already_account">Já tem conta?</string>
</resources>

View File

@@ -0,0 +1,40 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<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>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/secondary_color</item>
<item name="colorSecondaryVariant">@color/secondary_dark_color</item>
<item name="colorOnSecondary">@color/white</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<item name="android:windowLightStatusBar">false</item>
<!-- Button Style -->
<item name="materialButtonStyle">@style/Widget.Cuida.Button</item>
<!-- Card Style -->
<item name="materialCardViewStyle">@style/Widget.Cuida.CardView</item>
</style>
<style name="Widget.Cuida.Button" parent="Widget.MaterialComponents.Button">
<item name="cornerRadius">100dp</item>
<item name="android:paddingTop">12dp</item>
<item name="android:paddingBottom">12dp</item>
<item name="android:fontFamily">sans-serif-medium</item>
</style>
<style name="Widget.Cuida.CardView" parent="Widget.MaterialComponents.CardView">
<item name="cardCornerRadius">24dp</item>
<item name="cardElevation">0dp</item>
<item name="strokeWidth">1dp</item>
<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>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<include domain="sharedpref" path="."/>
<include domain="database" path="."/>
</full-backup-content>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<include domain="sharedpref" path="."/>
<include domain="database" path="."/>
</cloud-backup>
<device-transfer>
<include domain="sharedpref" path="."/>
<include domain="database" path="."/>
</device-transfer>
</data-extraction-rules>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="my_cache" path="." />
<external-path name="my_external" path="." />
</paths>