diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3df0cb2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# --- Ficheiros de Build e Binários --- +.gradle/ +build/ +bin/ +gen/ +out/ +captures/ +.externalNativeBuild/ +.cxx/ + +# --- Configurações Locais (NÃO partilhar, contêm caminhos do teu PC) --- +local.properties + +# --- Android Studio / IntelliJ --- +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +.idea/caches +.idea/modules.xml +.idea/navEditor.xml +.idea/vcs.xml +.idea/dbnavigator.xml + +# --- Ficheiros de Log e Cache --- +*.log +.navigation/ + +# --- Segredos e Chaves (Segurança Crítica) --- +# Se usares ficheiros de chaves para assinar a App, nunca os metas no Git! +*.jks +*.keystore +*.pk8 +*.p12 +google-services.json (opcional: depende se queres partilhar config do Firebase) + +# --- Ficheiros de Sistema --- +.DS_Store +Thumbs.db + +# --- Bundle Tool --- +*.apks +*.aab diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..fd12fb0 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Cuida \ No newline at end of file diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml new file mode 100644 index 0000000..371f2e2 --- /dev/null +++ b/.idea/appInsightsSettings.xml @@ -0,0 +1,26 @@ + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..9b1dd1a --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..9cd689a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/studiobot.xml b/.idea/studiobot.xml new file mode 100644 index 0000000..539e3b8 --- /dev/null +++ b/.idea/studiobot.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c5f3f6b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..3959019 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,75 @@ +plugins { + id 'com.android.application' + id 'com.google.gms.google-services' +} + +android { + namespace 'com.example.cuida' + compileSdk 35 + + defaultConfig { + applicationId "com.example.cuida" + minSdk 24 + targetSdk 35 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + + + buildFeatures { + viewBinding true + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.ai.client.generativeai:generativeai:0.9.0' + implementation 'com.google.android.material:material:1.11.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0' + implementation 'androidx.navigation:navigation-fragment:2.7.7' + implementation 'androidx.navigation:navigation-ui:2.7.7' + + // Adiciona a biblioteca para Auth se for do Google ID (credentials) + implementation 'androidx.biometric:biometric:1.1.0' + implementation 'androidx.credentials:credentials:1.5.0' + implementation 'androidx.credentials:credentials-play-services-auth:1.5.0' + //noinspection UseIdentifyId + implementation 'com.google.android.libraries.identity.googleid:googleid:1.1.1' + + + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + + // Location + implementation 'com.google.android.gms:play-services-location:21.0.1' + + // Para chamadas de rede e JSON (Nova solução oficial de comunicação com a IA) + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.retrofit2:converter-gson:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.10.0' + + // Firebase BoM + implementation platform('com.google.firebase:firebase-bom:32.7.2') + + // Firebase Auth and Firestore + implementation 'com.google.firebase:firebase-auth' + implementation 'com.google.firebase:firebase-firestore' + implementation 'com.google.firebase:firebase-database' +} diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 0000000..89c6514 --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,40 @@ +{ + "project_info": { + "project_number": "844909242089", + "firebase_url": "https://cuidamais-7b904-default-rtdb.firebaseio.com", + "project_id": "cuidamais-7b904", + "storage_bucket": "cuidamais-7b904.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:844909242089:android:4a039a7dbec802836ab278", + "android_client_info": { + "package_name": "com.example.cuida" + } + }, + "oauth_client": [ + { + "client_id": "844909242089-lvu2bh4u7hih6bm2a86rmdargnm4ul60.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCrTuHBRLoHkockoJEgAI9O7-gQJT6CkW4" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "844909242089-lvu2bh4u7hih6bm2a86rmdargnm4ul60.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6e721bd --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/example/cuida/CuidaApplication.java b/app/src/main/java/com/example/cuida/CuidaApplication.java new file mode 100644 index 0000000..75bfa26 --- /dev/null +++ b/app/src/main/java/com/example/cuida/CuidaApplication.java @@ -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); + } +} diff --git a/app/src/main/java/com/example/cuida/MainActivity.java b/app/src/main/java/com/example/cuida/MainActivity.java new file mode 100644 index 0000000..81eee15 --- /dev/null +++ b/app/src/main/java/com/example/cuida/MainActivity.java @@ -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); + } + } +} diff --git a/app/src/main/java/com/example/cuida/data/model/Appointment.java b/app/src/main/java/com/example/cuida/data/model/Appointment.java new file mode 100644 index 0000000..c85f045 --- /dev/null +++ b/app/src/main/java/com/example/cuida/data/model/Appointment.java @@ -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; + } +} diff --git a/app/src/main/java/com/example/cuida/data/model/Comprimido.java b/app/src/main/java/com/example/cuida/data/model/Comprimido.java new file mode 100644 index 0000000..f5abe0e --- /dev/null +++ b/app/src/main/java/com/example/cuida/data/model/Comprimido.java @@ -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; + } +} diff --git a/app/src/main/java/com/example/cuida/data/model/Medication.java b/app/src/main/java/com/example/cuida/data/model/Medication.java new file mode 100644 index 0000000..e80e9b6 --- /dev/null +++ b/app/src/main/java/com/example/cuida/data/model/Medication.java @@ -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; } +} diff --git a/app/src/main/java/com/example/cuida/data/model/Perfil.java b/app/src/main/java/com/example/cuida/data/model/Perfil.java new file mode 100644 index 0000000..a8e6ab9 --- /dev/null +++ b/app/src/main/java/com/example/cuida/data/model/Perfil.java @@ -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; + } +} diff --git a/app/src/main/java/com/example/cuida/data/model/User.java b/app/src/main/java/com/example/cuida/data/model/User.java new file mode 100644 index 0000000..d69bded --- /dev/null +++ b/app/src/main/java/com/example/cuida/data/model/User.java @@ -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; } + +} diff --git a/app/src/main/java/com/example/cuida/services/AlarmReceiver.java b/app/src/main/java/com/example/cuida/services/AlarmReceiver.java new file mode 100644 index 0000000..ed1cffb --- /dev/null +++ b/app/src/main/java/com/example/cuida/services/AlarmReceiver.java @@ -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"); + } + } + } +} diff --git a/app/src/main/java/com/example/cuida/services/Gemini.java b/app/src/main/java/com/example/cuida/services/Gemini.java new file mode 100644 index 0000000..aa6a9c2 --- /dev/null +++ b/app/src/main/java/com/example/cuida/services/Gemini.java @@ -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)); + } + } +} diff --git a/app/src/main/java/com/example/cuida/ui/appointments/AppointmentAdapter.java b/app/src/main/java/com/example/cuida/ui/appointments/AppointmentAdapter.java new file mode 100644 index 0000000..75ce7e6 --- /dev/null +++ b/app/src/main/java/com/example/cuida/ui/appointments/AppointmentAdapter.java @@ -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 { + + private List appointmentList = new ArrayList<>(); + + public void setAppointments(List 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); + } + } +} diff --git a/app/src/main/java/com/example/cuida/ui/appointments/AppointmentsFragment.java b/app/src/main/java/com/example/cuida/ui/appointments/AppointmentsFragment.java new file mode 100644 index 0000000..d8c856d --- /dev/null +++ b/app/src/main/java/com/example/cuida/ui/appointments/AppointmentsFragment.java @@ -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; + } +} diff --git a/app/src/main/java/com/example/cuida/ui/appointments/AppointmentsViewModel.java b/app/src/main/java/com/example/cuida/ui/appointments/AppointmentsViewModel.java new file mode 100644 index 0000000..020f076 --- /dev/null +++ b/app/src/main/java/com/example/cuida/ui/appointments/AppointmentsViewModel.java @@ -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> futureAppointments = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> 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 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 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> getFutureAppointments() { + return futureAppointments; + } + + public LiveData> 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)); + } +} diff --git a/app/src/main/java/com/example/cuida/ui/auth/ForgotPasswordActivity.java b/app/src/main/java/com/example/cuida/ui/auth/ForgotPasswordActivity.java new file mode 100644 index 0000000..7d3a099 --- /dev/null +++ b/app/src/main/java/com/example/cuida/ui/auth/ForgotPasswordActivity.java @@ -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()); + } +} diff --git a/app/src/main/java/com/example/cuida/ui/auth/LoginActivity.java b/app/src/main/java/com/example/cuida/ui/auth/LoginActivity.java new file mode 100644 index 0000000..b449d26 --- /dev/null +++ b/app/src/main/java/com/example/cuida/ui/auth/LoginActivity.java @@ -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(); + } +} diff --git a/app/src/main/java/com/example/cuida/ui/auth/RegisterActivity.java b/app/src/main/java/com/example/cuida/ui/auth/RegisterActivity.java new file mode 100644 index 0000000..7d63772 --- /dev/null +++ b/app/src/main/java/com/example/cuida/ui/auth/RegisterActivity.java @@ -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 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 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(); + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/cuida/ui/auth/ResetPasswordActivity.java b/app/src/main/java/com/example/cuida/ui/auth/ResetPasswordActivity.java new file mode 100644 index 0000000..6056835 --- /dev/null +++ b/app/src/main/java/com/example/cuida/ui/auth/ResetPasswordActivity.java @@ -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(); + } + }); + } +} diff --git a/app/src/main/java/com/example/cuida/ui/home/HomeFragment.java b/app/src/main/java/com/example/cuida/ui/home/HomeFragment.java new file mode 100644 index 0000000..8a2a27f --- /dev/null +++ b/app/src/main/java/com/example/cuida/ui/home/HomeFragment.java @@ -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; + } +} diff --git a/app/src/main/java/com/example/cuida/ui/medication/ComprimidoRecyclerAdapter.java b/app/src/main/java/com/example/cuida/ui/medication/ComprimidoRecyclerAdapter.java new file mode 100644 index 0000000..2320135 --- /dev/null +++ b/app/src/main/java/com/example/cuida/ui/medication/ComprimidoRecyclerAdapter.java @@ -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 { + + private List pills; + private OnItemClickListener listener; + + public interface OnItemClickListener { + void onItemClick(Comprimido comprimido); + } + + public ComprimidoRecyclerAdapter(List 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); + } + } +} diff --git a/app/src/main/java/com/example/cuida/ui/medication/MedicationAdapter.java b/app/src/main/java/com/example/cuida/ui/medication/MedicationAdapter.java new file mode 100644 index 0000000..d1ba491 --- /dev/null +++ b/app/src/main/java/com/example/cuida/ui/medication/MedicationAdapter.java @@ -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 { + + private List 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 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)); + } + }); + } + } +} diff --git a/app/src/main/java/com/example/cuida/ui/medication/MedicationDialog.java b/app/src/main/java/com/example/cuida/ui/medication/MedicationDialog.java new file mode 100644 index 0000000..84af82c --- /dev/null +++ b/app/src/main/java/com/example/cuida/ui/medication/MedicationDialog.java @@ -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 searchResults = new ArrayList<>(); + private List 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 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(); + } + } +} diff --git a/app/src/main/java/com/example/cuida/ui/medication/MedicationFragment.java b/app/src/main/java/com/example/cuida/ui/medication/MedicationFragment.java new file mode 100644 index 0000000..c98b8c6 --- /dev/null +++ b/app/src/main/java/com/example/cuida/ui/medication/MedicationFragment.java @@ -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; + } +} diff --git a/app/src/main/java/com/example/cuida/ui/medication/MedicationViewModel.java b/app/src/main/java/com/example/cuida/ui/medication/MedicationViewModel.java new file mode 100644 index 0000000..ebeaab9 --- /dev/null +++ b/app/src/main/java/com/example/cuida/ui/medication/MedicationViewModel.java @@ -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> allMedications = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData 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 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> getAllMedications() { + return allMedications; + } + + public LiveData 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)); + } +} diff --git a/app/src/main/java/com/example/cuida/ui/profile/ProfileFragment.java b/app/src/main/java/com/example/cuida/ui/profile/ProfileFragment.java new file mode 100644 index 0000000..f4641a7 --- /dev/null +++ b/app/src/main/java/com/example/cuida/ui/profile/ProfileFragment.java @@ -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 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(); + } +} diff --git a/app/src/main/java/com/example/cuida/ui/schedule/ScheduleAppointmentFragment.java b/app/src/main/java/com/example/cuida/ui/schedule/ScheduleAppointmentFragment.java new file mode 100644 index 0000000..3add774 --- /dev/null +++ b/app/src/main/java/com/example/cuida/ui/schedule/ScheduleAppointmentFragment.java @@ -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 shuffledDoctors = new java.util.ArrayList<>(doctors); + java.util.Collections.shuffle(shuffledDoctors); // Randomize the names as requested + ArrayAdapter 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); + } + }); + } +} diff --git a/app/src/main/java/com/example/cuida/ui/schedule/ScheduleViewModel.java b/app/src/main/java/com/example/cuida/ui/schedule/ScheduleViewModel.java new file mode 100644 index 0000000..1090604 --- /dev/null +++ b/app/src/main/java/com/example/cuida/ui/schedule/ScheduleViewModel.java @@ -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 selectedDate = new MutableLiveData<>(); + private final MutableLiveData selectedTime = new MutableLiveData<>(); + private final MutableLiveData selectedDoctor = new MutableLiveData<>(); + private final MutableLiveData> timeSlots = new MutableLiveData<>(); + private final MutableLiveData selectedDoctorSchedule = new MutableLiveData<>(); + private final MutableLiveData saveSuccess = new MutableLiveData<>(); + private final MutableLiveData saveError = new MutableLiveData<>(); + private final MutableLiveData> doctorsList = new MutableLiveData<>(new ArrayList<>()); + private final java.util.Map 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 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 getSelectedDoctorSchedule() { + return selectedDoctorSchedule; + } + + public LiveData getSelectedDate() { + return selectedDate; + } + + public void setTime(String time) { + selectedTime.setValue(time); + List currentSlots = timeSlots.getValue(); + if (currentSlots != null) { + for (TimeSlot slot : currentSlots) { + slot.setSelected(slot.getTime().equals(time)); + } + timeSlots.setValue(currentSlots); + } + } + + public LiveData getSelectedTime() { + return selectedTime; + } + + public LiveData> getTimeSlots() { + return timeSlots; + } + + public LiveData getSaveSuccess() { + return saveSuccess; + } + + public LiveData getSaveError() { + return saveError; + } + + public LiveData> 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 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 slots = generateTimeSlots(bookedTimes, date); + timeSlots.setValue(slots); + }); + } + + @Override + protected void onCleared() { + super.onCleared(); + if (snapshotListener != null) { + snapshotListener.remove(); + } + } + + private List generateTimeSlots(List bookedTimes, String selectedDateStr) { + List 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 slots, String time, List 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."); + }); + } + } +} diff --git a/app/src/main/java/com/example/cuida/ui/schedule/TimeSlot.java b/app/src/main/java/com/example/cuida/ui/schedule/TimeSlot.java new file mode 100644 index 0000000..76d1aaa --- /dev/null +++ b/app/src/main/java/com/example/cuida/ui/schedule/TimeSlot.java @@ -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); + } +} diff --git a/app/src/main/java/com/example/cuida/ui/schedule/TimeSlotAdapter.java b/app/src/main/java/com/example/cuida/ui/schedule/TimeSlotAdapter.java new file mode 100644 index 0000000..843039b --- /dev/null +++ b/app/src/main/java/com/example/cuida/ui/schedule/TimeSlotAdapter.java @@ -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 { + + private List 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 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); + } + } +} diff --git a/app/src/main/java/com/example/cuida/ui/sns24/Sns24Fragment.java b/app/src/main/java/com/example/cuida/ui/sns24/Sns24Fragment.java new file mode 100644 index 0000000..b2e2c1c --- /dev/null +++ b/app/src/main/java/com/example/cuida/ui/sns24/Sns24Fragment.java @@ -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 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; + } +} diff --git a/app/src/main/java/com/example/cuida/utils/AlarmScheduler.java b/app/src/main/java/com/example/cuida/utils/AlarmScheduler.java new file mode 100644 index 0000000..58ca542 --- /dev/null +++ b/app/src/main/java/com/example/cuida/utils/AlarmScheduler.java @@ -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(); + } +} diff --git a/app/src/main/java/com/example/cuida/utils/NotificationHelper.java b/app/src/main/java/com/example/cuida/utils/NotificationHelper.java new file mode 100644 index 0000000..1b9f1d5 --- /dev/null +++ b/app/src/main/java/com/example/cuida/utils/NotificationHelper.java @@ -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()); + } + } +} diff --git a/app/src/main/res/drawable-v26/ic_launcher_final.xml b/app/src/main/res/drawable-v26/ic_launcher_final.xml new file mode 100644 index 0000000..9047195 --- /dev/null +++ b/app/src/main/res/drawable-v26/ic_launcher_final.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_gradient_header.xml b/app/src/main/res/drawable/bg_gradient_header.xml new file mode 100644 index 0000000..2649a80 --- /dev/null +++ b/app/src/main/res/drawable/bg_gradient_header.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_search_results.xml b/app/src/main/res/drawable/bg_search_results.xml new file mode 100644 index 0000000..2a1c97d --- /dev/null +++ b/app/src/main/res/drawable/bg_search_results.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/btn_outline_error.xml b/app/src/main/res/drawable/btn_outline_error.xml new file mode 100644 index 0000000..9169e01 --- /dev/null +++ b/app/src/main/res/drawable/btn_outline_error.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/btn_outline_primary.xml b/app/src/main/res/drawable/btn_outline_primary.xml new file mode 100644 index 0000000..a4b75e7 --- /dev/null +++ b/app/src/main/res/drawable/btn_outline_primary.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher.xml b/app/src/main/res/drawable/ic_launcher.xml new file mode 100644 index 0000000..97295d6 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_launcher_final.xml b/app/src/main/res/drawable/ic_launcher_final.xml new file mode 100644 index 0000000..b795ad1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_final.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_launcher_round.xml b/app/src/main/res/drawable/ic_launcher_round.xml new file mode 100644 index 0000000..0f0ca05 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_round.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_logo.png b/app/src/main/res/drawable/ic_logo.png new file mode 100644 index 0000000..e7ad304 Binary files /dev/null and b/app/src/main/res/drawable/ic_logo.png differ diff --git a/app/src/main/res/drawable/ic_logo_scaled.xml b/app/src/main/res/drawable/ic_logo_scaled.xml new file mode 100644 index 0000000..62ac60c --- /dev/null +++ b/app/src/main/res/drawable/ic_logo_scaled.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_placeholder.xml b/app/src/main/res/drawable/ic_placeholder.xml new file mode 100644 index 0000000..6bdced2 --- /dev/null +++ b/app/src/main/res/drawable/ic_placeholder.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_forgot_password.xml b/app/src/main/res/layout/activity_forgot_password.xml new file mode 100644 index 0000000..b1137ad --- /dev/null +++ b/app/src/main/res/layout/activity_forgot_password.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..2d3e660 --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..ebe93ee --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/app/src/main/res/layout/activity_register.xml b/app/src/main/res/layout/activity_register.xml new file mode 100644 index 0000000..019ec56 --- /dev/null +++ b/app/src/main/res/layout/activity_register.xml @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_reset_password.xml b/app/src/main/res/layout/activity_reset_password.xml new file mode 100644 index 0000000..57b94a0 --- /dev/null +++ b/app/src/main/res/layout/activity_reset_password.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_add_medication.xml b/app/src/main/res/layout/dialog_add_medication.xml new file mode 100644 index 0000000..339cb19 --- /dev/null +++ b/app/src/main/res/layout/dialog_add_medication.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_change_password.xml b/app/src/main/res/layout/dialog_change_password.xml new file mode 100644 index 0000000..053d44c --- /dev/null +++ b/app/src/main/res/layout/dialog_change_password.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_edit_profile.xml b/app/src/main/res/layout/dialog_edit_profile.xml new file mode 100644 index 0000000..377fc5d --- /dev/null +++ b/app/src/main/res/layout/dialog_edit_profile.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_appointments.xml b/app/src/main/res/layout/fragment_appointments.xml new file mode 100644 index 0000000..44990b1 --- /dev/null +++ b/app/src/main/res/layout/fragment_appointments.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml new file mode 100644 index 0000000..f2cf4dc --- /dev/null +++ b/app/src/main/res/layout/fragment_home.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_medication.xml b/app/src/main/res/layout/fragment_medication.xml new file mode 100644 index 0000000..5977667 --- /dev/null +++ b/app/src/main/res/layout/fragment_medication.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_profile.xml b/app/src/main/res/layout/fragment_profile.xml new file mode 100644 index 0000000..b0bd257 --- /dev/null +++ b/app/src/main/res/layout/fragment_profile.xml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_schedule_appointment.xml b/app/src/main/res/layout/fragment_schedule_appointment.xml new file mode 100644 index 0000000..62b0e21 --- /dev/null +++ b/app/src/main/res/layout/fragment_schedule_appointment.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +