falta o esqueci a palavra passe

This commit is contained in:
2026-04-22 12:42:02 +01:00
parent d3e2af3a66
commit 1ed42f8c28
921 changed files with 109065 additions and 2837 deletions

View File

@@ -1,42 +1,31 @@
package com.example.cuida.services;
import com.google.ai.client.generativeai.GenerativeModel;
import com.google.ai.client.generativeai.java.GenerativeModelFutures;
import com.google.ai.client.generativeai.type.Content;
import com.google.ai.client.generativeai.type.GenerateContentResponse;
import com.google.ai.client.generativeai.type.HarmCategory;
import com.google.ai.client.generativeai.type.SafetySetting;
import com.google.ai.client.generativeai.type.BlockThreshold;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import android.os.Handler;
import android.os.Looper;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
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 final GenerativeModelFutures modelo;
private static final String API_KEY = "AIzaSyBmLgn-SHaTDvAeDWsw2iTZRR9gahhOu7k";
private static final String MODEL_NAME = "gemini-flash-latest"; // O gemini-1.5-flash foi descontinuado e removido da API
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() {
// 1. Configurar Segurança (Padrão para evitar bloqueio de termos médicos)
List<SafetySetting> safetySettings = Arrays.asList(
new SafetySetting(HarmCategory.HARASSMENT, BlockThreshold.NONE),
new SafetySetting(HarmCategory.HATE_SPEECH, BlockThreshold.NONE),
new SafetySetting(HarmCategory.SEXUALLY_EXPLICIT, BlockThreshold.NONE),
new SafetySetting(HarmCategory.DANGEROUS_CONTENT, BlockThreshold.NONE)
);
// 2. Modelo (Simplificado ao máximo para garantir compilação)
GenerativeModel generativeModel = new GenerativeModel(
"gemini-1.5-flash",
"AIzaSyBmLgn-SHaTDvAeDWsw2iTZRR9gahhOu7k",
null, // Usamos o GenerationConfig padrão para evitar erro de setTemperature
safetySettings
);
this.modelo = GenerativeModelFutures.from(generativeModel);
this.client = new OkHttpClient();
this.mainHandler = new Handler(Looper.getMainLooper());
}
public interface GeminiCallback {
@@ -45,27 +34,56 @@ public class Gemini {
}
public void fazerPergunta(String promptUtilizador, GeminiCallback callback) {
Content conteudo = new Content.Builder()
.addText(promptUtilizador)
.build();
try {
JSONObject jsonBody = new JSONObject();
JSONArray contents = new JSONArray();
JSONObject content = new JSONObject();
JSONArray parts = new JSONArray();
JSONObject part = new JSONObject();
ListenableFuture<GenerateContentResponse> respostaFuture = modelo.generateContent(conteudo);
Executor executor = Executors.newSingleThreadExecutor();
part.put("text", promptUtilizador);
parts.put(part);
content.put("parts", parts);
contents.put(content);
jsonBody.put("contents", contents);
Futures.addCallback(respostaFuture, new FutureCallback<GenerateContentResponse>() {
@Override
public void onSuccess(GenerateContentResponse resultado) {
if (resultado != null && resultado.getText() != null) {
callback.onSuccess(resultado.getText());
} else {
callback.onError(new Exception("Resposta vazia da IA"));
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 onFailure(Throwable t) {
callback.onError(t);
}
}, executor);
@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

@@ -11,18 +11,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.Toast;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.pdf.PdfDocument;
import androidx.core.content.FileProvider;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.example.cuida.data.model.Medication;
import com.example.cuida.data.model.Appointment;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
@@ -59,7 +48,6 @@ public class ProfileFragment extends Fragment {
getActivity().finish();
});
binding.buttonExportReport.setOnClickListener(v -> exportMonthlyReport());
return binding.getRoot();
}
@@ -310,119 +298,4 @@ public class ProfileFragment extends Fragment {
dialog.show();
}
private void exportMonthlyReport() {
if (currentUser == null || auth.getCurrentUser() == null) return;
String userId = auth.getCurrentUser().getUid();
Toast.makeText(getContext(), "A gerar relatório...", Toast.LENGTH_SHORT).show();
db.collection("medicamentos").whereEqualTo("userId", userId).get()
.addOnSuccessListener(medSnapshots -> {
List<Medication> meds = new ArrayList<>();
for (com.google.firebase.firestore.DocumentSnapshot doc : medSnapshots) {
meds.add(doc.toObject(Medication.class));
}
db.collection("consultas").whereEqualTo("userId", userId).get()
.addOnSuccessListener(apptSnapshots -> {
List<Appointment> appts = new ArrayList<>();
for (com.google.firebase.firestore.DocumentSnapshot doc : apptSnapshots) {
appts.add(doc.toObject(Appointment.class));
}
generateAndSharePDF(meds, appts);
});
});
}
private void generateAndSharePDF(List<Medication> meds, List<Appointment> appts) {
PdfDocument document = new PdfDocument();
PdfDocument.PageInfo pageInfo = new PdfDocument.PageInfo.Builder(595, 842, 1).create(); // A4 size
PdfDocument.Page page = document.startPage(pageInfo);
Canvas canvas = page.getCanvas();
Paint paint = new Paint();
int y = 50;
paint.setTextSize(24);
paint.setFakeBoldText(true);
canvas.drawText("Relatório Mensal de Saúde - Cuida+", 50, y, paint);
y += 40;
paint.setTextSize(14);
paint.setFakeBoldText(false);
canvas.drawText("Utilizador: " + currentUser.name, 50, y, paint);
y += 20;
canvas.drawText("Data: " + java.text.DateFormat.getDateTimeInstance().format(new java.util.Date()), 50, y, paint);
y += 40;
paint.setFakeBoldText(true);
paint.setTextSize(18);
canvas.drawText("Medicação Atual:", 50, y, paint);
y += 25;
paint.setFakeBoldText(false);
paint.setTextSize(12);
if (meds.isEmpty()) {
canvas.drawText("Nenhuma medicação registada.", 70, y, paint);
y += 20;
} else {
for (Medication m : meds) {
canvas.drawText("- " + m.name + " (" + m.dosage + ") - Horários: " + m.time, 70, y, paint);
y += 20;
if (m.notes != null && !m.notes.isEmpty()) {
paint.setColor(Color.GRAY);
canvas.drawText(" Notas: " + m.notes, 70, y, paint);
paint.setColor(Color.BLACK);
y += 20;
}
if (y > 750) break; // Simple page break check
}
}
y += 20;
paint.setFakeBoldText(true);
paint.setTextSize(18);
canvas.drawText("Consultas Agendadas:", 50, y, paint);
y += 25;
paint.setFakeBoldText(false);
paint.setTextSize(12);
if (appts.isEmpty()) {
canvas.drawText("Nenhuma consulta registada.", 70, y, paint);
} else {
for (Appointment a : appts) {
canvas.drawText("- " + a.type + " em " + a.date + " às " + a.time, 70, y, paint);
y += 20;
canvas.drawText(" Motivo: " + a.reason, 70, y, paint);
y += 25;
if (y > 800) break;
}
}
document.finishPage(page);
File file = new File(requireContext().getCacheDir(), "Relatorio_Saude_Cuida.pdf");
try {
document.writeTo(new FileOutputStream(file));
document.close();
shareFile(file);
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(getContext(), "Erro ao gerar PDF", Toast.LENGTH_SHORT).show();
document.close();
}
}
private void shareFile(File file) {
android.net.Uri uri = FileProvider.getUriForFile(requireContext(),
requireContext().getPackageName() + ".fileprovider", file);
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setDataAndType(uri, "application/pdf");
intent.putExtra(Intent.EXTRA_SUBJECT, "Relatório de Saúde Cuida+");
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(intent, "Partilhar Relatório"));
}
}

View File

@@ -115,6 +115,11 @@ public class ScheduleAppointmentFragment extends Fragment {
spinnerDoctor.setAdapter(adapter);
}
});
spinnerDoctor.setOnItemClickListener((parent, view, position, id) -> {
String selectedDoctor = (String) parent.getItemAtPosition(position);
scheduleViewModel.setSelectedDoctor(selectedDoctor);
});
}
private void setupDatePicker() {
@@ -181,5 +186,15 @@ public class ScheduleAppointmentFragment extends Fragment {
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();
}
});
}
}

View File

@@ -12,6 +12,7 @@ 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;
@@ -24,9 +25,13 @@ public class ScheduleViewModel extends AndroidViewModel {
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<Boolean> saveSuccess = new MutableLiveData<>();
private final MutableLiveData<String> saveError = new MutableLiveData<>();
private final MutableLiveData<List<String>> doctorsList = new MutableLiveData<>(new ArrayList<>());
private ListenerRegistration snapshotListener;
public ScheduleViewModel(@NonNull Application application) {
super(application);
@@ -43,16 +48,19 @@ public class ScheduleViewModel extends AndroidViewModel {
List<String> docs = new ArrayList<>();
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");
if (name != null) {
if (name != null && !name.trim().isEmpty()) {
String displayName = name;
if (specialty != null && !specialty.isEmpty()) {
if (specialty != null && !specialty.trim().isEmpty()) {
displayName += " - " + specialty;
}
if (!displayName.startsWith("Dr.") && !displayName.startsWith("Dra.")) {
if ("Feminino".equalsIgnoreCase(gender)) {
if ("Feminino".equalsIgnoreCase(gender) || "Feminino".equals(gender)) {
displayName = "Dra. " + displayName;
} else {
displayName = "Dr. " + displayName;
@@ -74,6 +82,14 @@ public class ScheduleViewModel extends AndroidViewModel {
loadTimeSlots(date);
}
public void setSelectedDoctor(String doctor) {
selectedDoctor.setValue(doctor);
String date = selectedDate.getValue();
if (date != null) {
loadTimeSlots(date);
}
}
public LiveData<String> getSelectedDate() {
return selectedDate;
}
@@ -101,40 +117,64 @@ public class ScheduleViewModel extends AndroidViewModel {
return saveSuccess;
}
public LiveData<String> getSaveError() {
return saveError;
}
public LiveData<List<String>> getDoctorsList() {
return doctorsList;
}
private void loadTimeSlots(String date) {
// Init slots immediately to prevent "disappearing" hours while waiting for
// network.
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;
if (auth.getCurrentUser() == null) return;
String userId = auth.getCurrentUser().getUid();
String doctor = selectedDoctor.getValue();
db.collection("consultas")
.whereEqualTo("userId", userId)
// Listen in REAL-TIME for all appointments on the selected date
snapshotListener = db.collection("consultas")
.whereEqualTo("date", date)
.get()
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
List<String> bookedTimes = new ArrayList<>();
for (QueryDocumentSnapshot document : task.getResult()) {
.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);
if (appt.time != null) {
bookedTimes.add(appt.time);
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);
} else {
Log.e("ScheduleViewModel", "Error getting booked slots", task.getException());
}
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;
@@ -184,47 +224,65 @@ public class ScheduleViewModel extends AndroidViewModel {
String userId = auth.getCurrentUser().getUid();
if (date != null && time != null) {
Appointment appointment = new Appointment(type, date, time, reason, false, userId);
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")
.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]);
.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");
String[] timeParts = time.split(":");
int hour = Integer.parseInt(timeParts[0]);
int minute = Integer.parseInt(timeParts[1]);
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]);
Calendar baseCal = Calendar.getInstance();
baseCal.set(year, month, day, hour, minute, 0);
String[] timeParts = time.split(":");
int hour = Integer.parseInt(timeParts[0]);
int minute = Integer.parseInt(timeParts[1]);
// 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());
}
Calendar baseCal = Calendar.getInstance();
baseCal.set(year, month, day, hour, minute, 0);
// 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));
// 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

@@ -68,10 +68,11 @@ public class Sns24Fragment extends Fragment {
binding.textAiResult.setText("A analisar sintomas...");
binding.buttonFindHospital.setVisibility(View.GONE);
String prompt = "Atua como um assistente de triagem médica do SNS 24. " +
"Analisa os seguintes sintomas de forma curta e direta. " +
"Se os sintomas parecerem graves, começa a resposta com [GRAVE]. " +
"Sintomas: " + symptoms;
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

View File

@@ -115,15 +115,6 @@
android:layout_marginBottom="16dp"
android:backgroundTint="@color/secondary_color"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_export_report"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="Exportar Relatório Mensal"
android:layout_marginBottom="16dp"
app:strokeColor="@color/secondary_color"
android:textColor="@color/secondary_color"/>
<com.google.android.material.button.MaterialButton