commit 0c54f9a9cc57737bea4cec5a3d990bc410554fdd Author: 230410 <230410@epvc.pt> Date: Fri Dec 12 10:22:34 2025 +0000 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.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..092cff1 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Bem+ \ 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/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..afe9e82 --- /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/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ 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..74dd639 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ 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/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/ADICIONAR_LOGO.md b/ADICIONAR_LOGO.md new file mode 100644 index 0000000..ddcd882 --- /dev/null +++ b/ADICIONAR_LOGO.md @@ -0,0 +1,119 @@ +# 🎨 Como Adicionar o Logo Bem+ à App + +## 📍 Passo 1: Preparar a Imagem do Logo + +Precisa do logo em **diferentes tamanhos** para Android: + +### Tamanhos necessários: +- **mdpi**: 48x48 px +- **hdpi**: 72x72 px +- **xhdpi**: 96x96 px +- **xxhdpi**: 144x144 px +- **xxxhdpi**: 192x192 px + +### 🔧 Opção A: Gerar automaticamente (RECOMENDADO) + +1. Vá para [icon.kitchen](https://icon.kitchen) ou [appicon.co](https://appicon.co) +2. Faça upload da imagem do logo Bem+ +3. Escolha **"Android"** +4. Download do pacote ZIP com todos os tamanhos +5. Descompacte o ZIP + +### 🔧 Opção B: Android Studio (MAIS FÁCIL) + +1. Abra o projeto no Android Studio +2. Clique direito em `app/src/main/res` +3. **"New"** → **"Image Asset"** +4. **"Icon Type"**: Launcher Icons (Adaptive and Legacy) +5. **"Path"**: Selecione a imagem do logo +6. **"Name"**: `ic_launcher` +7. Clique **"Next"** → **"Finish"** +8. ✅ Android Studio gera TODOS os tamanhos automaticamente! + +--- + +## 📍 Passo 2: Colocar os Ficheiros nas Pastas Corretas + +### Se usou ferramentas online (Opção A): + +Copie os ficheiros para: + +``` +/Users/230410/AndroidStudioProjects/Bem/app/src/main/res/ + +mipmap-mdpi/ + └── ic_launcher.png (48x48) + +mipmap-hdpi/ + └── ic_launcher.png (72x72) + +mipmap-xhdpi/ + └── ic_launcher.png (96x96) + +mipmap-xxhdpi/ + └── ic_launcher.png (144x144) + +mipmap-xxxhdpi/ + └── ic_launcher.png (192x192) +``` + +### Também substitua os ícones redondos: + +``` +mipmap-mdpi/ + └── ic_launcher_round.png + +mipmap-hdpi/ + └── ic_launcher_round.png + +(... mesmos tamanhos que acima) +``` + +--- + +## 📍 Passo 3: Logo para usar DENTRO da App + +Para mostrar o logo nas telas internas: + +1. Pegue a imagem em **alta resolução** (pelo menos 512x512) +2. Renomeie para: `logo_bem.png` +3. Cole em: + ``` + /Users/230410/AndroidStudioProjects/Bem/app/src/main/res/drawable/logo_bem.png + ``` + +--- + +## ✅ Verificar no Terminal + +Depois de adicionar, execute: + +```bash +cd /Users/230410/AndroidStudioProjects/Bem +ls -la app/src/main/res/mipmap-xhdpi/ic_launcher.png +ls -la app/src/main/res/drawable/logo_bem.png +``` + +Se os ficheiros aparecerem, está tudo correto! ✓ + +--- + +## 🚀 Já Preparei o Código! + +Quando adicionar as imagens, a app VAI USAR AUTOMATICAMENTE: + +### Nas telas internas: +- ✅ Tela de login +- ✅ Cabeçalho da aba Alarmes +- ✅ Activity de alarme a tocar +- ✅ Diálogos + +### Como ícone da app: +- ✅ Ecrã principal (launcher) +- ✅ Notificações +- ✅ Recentes/multitarefa + +**Não precisa programar nada!** Só adicionar as imagens. 🎉 + + + diff --git a/COMO_USAR.md b/COMO_USAR.md new file mode 100644 index 0000000..61ca3b8 --- /dev/null +++ b/COMO_USAR.md @@ -0,0 +1,172 @@ +# 📱 Bem+ - Guia de Utilização Completo + +## 🎯 Visão Geral + +A app **Bem+** é um sistema completo de gestão de medicação para idosos com controlo parental integrado. + +--- + +## 👤 PARA UTILIZADORES (Idosos) + +### 1️⃣ Primeiro Acesso + +1. Abra a app +2. Toque em **"Registar"** +3. Preencha: + - Nome completo + - Email + - Palavra-passe (mínimo 6 caracteres) +4. Toque em **"Criar Conta"** + +### 2️⃣ Criar Alarmes + +1. Na aba **"Alarmes"** (sino verde) +2. Toque no botão **"+ Novo"** +3. Configure: + - Nome do medicamento (ex: Aspirina) + - Horário (toque para escolher com picker) + - Dosagem (ex: 100mg • Segunda a Sexta) +4. Toque **"Guardar"** +5. ✅ Alarme criado e agendado! + +### 3️⃣ Editar Alarmes + +Toque no ícone do **lápis** ✏️ em qualquer alarme: + +- **⏰ Alterar horário** - Mude a hora +- **🔔 Escolher toque** - Escolha som dos toques do telemóvel +- **🎵 Testar toque atual** - Ouve 5 segundos de pré-visualização +- **📝 Editar título** - Renomeie o medicamento + +### 4️⃣ Quando o Alarme Toca + +O ecrã liga automaticamente e mostra: + +- 💊 Nome do medicamento +- 🕐 Hora atual +- **⏰ Adiar 5min** - Toca novamente em 5 minutos +- **✓ Parar** - Para o alarme + +### 5️⃣ Confirmar que Tomou + +Em cada alarme, toque no botão **"✓ Confirmar"**: +- Regista a hora exata +- Notifica o responsável automaticamente +- Botão muda para **"✓ Tomado"** (cinza) +- Reseta amanhã + +### 6️⃣ Adicionar Responsável + +1. Toque no ícone de **⚙️ Configurações** (canto superior direito) +2. Escolha **"🎟️ Gerar código para responsável"** +3. Partilhe o código de **6 dígitos** com o responsável +4. ⏱️ **Válido por 30 segundos!** + +--- + +## 👨‍⚕️ PARA RESPONSÁVEIS + +### 1️⃣ Criar Conta de Responsável + +1. Na tela de login, toque em **"🔒 Sou Responsável"** +2. Toque em **"Registar"** +3. Preencha: + - Nome completo + - Email + - Palavra-passe + - Número de telemóvel +4. Toque em **"Criar Conta"** + +### 2️⃣ Vincular a um Utilizador + +Após criar conta: + +1. Peça ao utilizador para gerar um código +2. Na tela de código, insira o código de **6 dígitos** +3. Toque **"Vincular Responsável"** +4. ✅ Vinculado com sucesso! + +### 3️⃣ Monitorizar Medicação + +Aceda à aba **"Responsáveis"** (escudo roxo): + +**Dashboard em tempo real:** +- **Tomados**: Quantos medicamentos foram confirmados hoje +- **Pendentes**: Quantos ainda faltam +- **Adesão**: Percentagem de cumprimento + +**Lista detalhada:** +- ✅ **"✓ Tomado às 14:32"** (verde) - Confirmado +- ⚠️ **"⚠ Pendente"** (vermelho) - Ainda não tomou + +--- + +## 🔐 Segurança + +### Recuperar Palavra-passe + +1. Na tela de login, toque em **"Esqueci-me da palavra-passe"** +2. Insira o email +3. Toque **"Enviar"** +4. Verifique o email com link de recuperação + +### Múltiplos Responsáveis + +- Um utilizador pode ter **vários responsáveis** +- Cada responsável vê os mesmos dados +- Todos recebem notificações de confirmação + +### Códigos Temporários + +- ⏱️ Válidos por **30 segundos** +- 🔒 Uso único (não pode ser reutilizado) +- 🎲 Aleatórios de 6 dígitos +- ♻️ Pode gerar novos códigos ilimitadamente + +--- + +## 🎨 Abas da App + +### 🔔 Alarmes +- Criar, editar e apagar alarmes +- Confirmar tomada de medicamentos +- Escolher toques personalizados + +### 💚 Lembretes +- Dicas inteligentes baseadas no clima +- Sugestões de atividades +- Sistema contextual automático + +### 🛡️ Responsáveis (Controlo Parental) +- Monitorização em tempo real +- Histórico de medicações +- Estatísticas de adesão +- **Só acessível por responsáveis autenticados!** + +--- + +## ⚡ Dicas + +- 🔊 **Toques tocam MUITO ALTO** - Use volume máximo do canal de alarme +- 🔋 **Vibração otimizada** - Padrão intermitente poupa bateria +- 📱 **Funciona mesmo bloqueado** - Ecrã liga automaticamente +- ☁️ **Dados na cloud** - Firebase sincroniza tudo + +--- + +## 🆘 Problemas Comuns + +**"Alarme não toca"** +→ Verifique permissões de notificação e alarmes exatos + +**"Código expirou"** +→ São só 30 segundos! Gere novo código + +**"Não consigo entrar no Controlo Parental"** +→ Só responsáveis registados têm acesso + +**"Esqueci-me da palavra-passe"** +→ Use "Recuperar palavra-passe" com o email + + + diff --git a/FIREBASE_SETUP.md b/FIREBASE_SETUP.md new file mode 100644 index 0000000..f7b1ad5 --- /dev/null +++ b/FIREBASE_SETUP.md @@ -0,0 +1,93 @@ +# 🔥 Configuração do Firebase para Bem+ + +## Passo 1: Criar Projeto no Firebase Console + +1. Aceda a [console.firebase.google.com](https://console.firebase.google.com) +2. Clique em **"Adicionar projeto"** +3. Nome do projeto: **Bem Plus** (ou outro à escolha) +4. Desative Google Analytics (opcional) +5. Clique em **"Criar projeto"** + +## Passo 2: Adicionar App Android + +1. No painel do projeto, clique no ícone **Android** (robô verde) +2. **Nome do pacote**: `com.example.bem` +3. **Nome da app**: `Bem+` +4. Clique em **"Registar app"** + +## Passo 3: Transferir google-services.json + +1. Faça download do ficheiro **google-services.json** +2. Cole o ficheiro em: + ``` + /Users/230410/AndroidStudioProjects/Bem/app/google-services.json + ``` +3. **IMPORTANTE**: O ficheiro DEVE estar dentro da pasta `app/` + +## Passo 4: Ativar Authentication + +1. No menu lateral, clique em **"Authentication"** +2. Clique em **"Começar"** +3. Ative o método: **"Email/Password"** +4. Clique em **"Ativar"** e depois **"Guardar"** + +## Passo 5: Ativar Firestore Database + +1. No menu lateral, clique em **"Firestore Database"** +2. Clique em **"Criar base de dados"** +3. Selecione **"Modo de teste"** (permite leitura/escrita por 30 dias) +4. Localização: **"europe-west1"** (Frankfurt - mais próximo de Portugal) +5. Clique em **"Ativar"** + +## Passo 6: Configurar Regras de Segurança (Opcional mas recomendado) + +No Firestore, vá para **"Regras"** e cole: + +```javascript +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + match /users/{userId} { + allow read, write: if request.auth != null && request.auth.uid == userId; + allow read: if request.auth != null && + request.auth.uid in resource.data.guardians; + } + + match /inviteCodes/{code} { + allow read, write: if request.auth != null; + allow delete: if false; + } + } +} +``` + +Clique em **"Publicar"**. + +## Passo 7: Compilar a App + +Após seguir TODOS os passos acima: + +```bash +cd /Users/230410/AndroidStudioProjects/Bem +./gradlew assembleDebug +``` + +## ✅ Verificação + +Se tudo estiver correto: +- ✅ Ficheiro `google-services.json` na pasta `app/` +- ✅ Authentication ativo com Email/Password +- ✅ Firestore Database criado +- ✅ Build sem erros + +## 🚀 Pronto! + +A app agora tem: +- 🔐 Login/Registo de utilizadores +- 👥 Login de responsáveis +- 🎟️ Códigos temporários de 30 segundos +- 🔄 Recuperação de palavra-passe +- 📊 Painel de monitorização protegido + + + diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..8db86d3 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,51 @@ +plugins { + alias(libs.plugins.android.application) + id("com.google.gms.google-services") +} + +android { + namespace = "com.example.bem" + compileSdk = 36 + + defaultConfig { + applicationId = "com.example.bem" + minSdk = 24 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +dependencies { + + implementation(libs.appcompat) + implementation(libs.material) + implementation(libs.activity) + implementation(libs.constraintlayout) + + // Firebase + implementation(platform("com.google.firebase:firebase-bom:32.7.0")) + implementation("com.google.firebase:firebase-auth") + implementation("com.google.firebase:firebase-firestore") + implementation("com.google.firebase:firebase-analytics") + + testImplementation(libs.junit) + androidTestImplementation(libs.ext.junit) + androidTestImplementation(libs.espresso.core) +} \ No newline at end of file diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 0000000..3a8ec62 --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "785325517224", + "project_id": "bem-firebase", + "storage_bucket": "bem-firebase.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:785325517224:android:705886336a3809fc3f159d", + "android_client_info": { + "package_name": "com.example.bem" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyAe0XbtC_eQY93MWyWzu8I_d1ALOZkBDgQ" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/bem/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/example/bem/ExampleInstrumentedTest.java new file mode 100644 index 0000000..c1d5ddc --- /dev/null +++ b/app/src/androidTest/java/com/example/bem/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.example.bem; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.example.bem", appContext.getPackageName()); + } +} \ 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..98c733b --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..2adebc2 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/example/bem/AlarmReceiver.java b/app/src/main/java/com/example/bem/AlarmReceiver.java new file mode 100644 index 0000000..26fdf3b --- /dev/null +++ b/app/src/main/java/com/example/bem/AlarmReceiver.java @@ -0,0 +1,119 @@ +package com.example.bem; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Build; + +import androidx.core.app.NotificationCompat; + +public class AlarmReceiver extends BroadcastReceiver { + + private static final String CHANNEL_ID = "alarm_channel"; + private static final int NOTIFICATION_ID = 1001; + + @Override + public void onReceive(Context context, Intent intent) { + String title = intent.getStringExtra("alarm_title"); + String message = intent.getStringExtra("alarm_message"); + String ringtoneUriString = intent.getStringExtra("ringtone_uri"); + int alarmId = intent.getIntExtra("alarm_id", -1); + + if (title == null) { + title = "Alarme de Medicamento"; + } + if (message == null) { + message = "Hora de tomar o seu medicamento!"; + } + + Intent activityIntent = new Intent(context, AlarmRingingActivity.class); + activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + Intent.FLAG_ACTIVITY_CLEAR_TOP | + Intent.FLAG_ACTIVITY_SINGLE_TOP); + activityIntent.putExtra("alarm_title", title); + activityIntent.putExtra("alarm_time", new java.text.SimpleDateFormat("HH:mm", java.util.Locale.getDefault()).format(new java.util.Date())); + activityIntent.putExtra("ringtone_uri", ringtoneUriString); + activityIntent.putExtra("alarm_id", alarmId); + context.startActivity(activityIntent); + + Intent serviceIntent = new Intent(context, AlarmSoundService.class); + serviceIntent.putExtra("alarm_title", title); + serviceIntent.putExtra("ringtone_uri", ringtoneUriString); + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + context.startForegroundService(serviceIntent); + } else { + context.startService(serviceIntent); + } + + createNotificationChannel(context); + showNotification(context, title, message, ringtoneUriString, alarmId); + } + + private void createNotificationChannel(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + CharSequence name = "Alarmes de Medicação"; + String description = "Notificações para lembretes de medicamentos"; + int importance = NotificationManager.IMPORTANCE_HIGH; + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance); + channel.setDescription(description); + channel.enableVibration(true); + channel.enableLights(true); + + NotificationManager notificationManager = context.getSystemService(NotificationManager.class); + if (notificationManager != null) { + notificationManager.createNotificationChannel(channel); + } + } + } + + private void showNotification(Context context, String title, String message, String ringtoneUriString, int alarmId) { + Intent activityIntent = new Intent(context, AlarmRingingActivity.class); + activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + activityIntent.putExtra("alarm_title", title); + activityIntent.putExtra("alarm_time", new java.text.SimpleDateFormat("HH:mm", java.util.Locale.getDefault()).format(new java.util.Date())); + activityIntent.putExtra("ringtone_uri", ringtoneUriString); + activityIntent.putExtra("alarm_id", alarmId); + + PendingIntent pendingIntent = PendingIntent.getActivity( + context, + alarmId >= 0 ? alarmId : 0, + activityIntent, + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + ); + + Intent stopIntent = new Intent(context, AlarmSoundService.class); + stopIntent.setAction("STOP_ALARM"); + PendingIntent stopPendingIntent = PendingIntent.getService( + context, + alarmId >= 0 ? alarmId + 1000 : 1000, + stopIntent, + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + ); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(android.R.drawable.ic_lock_idle_alarm) + .setContentTitle("⏰ " + title) + .setContentText(message) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setCategory(NotificationCompat.CATEGORY_ALARM) + .setFullScreenIntent(pendingIntent, true) + .setOngoing(true) + .setAutoCancel(false) + .setContentIntent(pendingIntent) + .addAction(android.R.drawable.ic_media_pause, "Parar", stopPendingIntent); + + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager != null) { + notificationManager.notify(NOTIFICATION_ID, builder.build()); + } + } + +} + diff --git a/app/src/main/java/com/example/bem/AlarmRingingActivity.java b/app/src/main/java/com/example/bem/AlarmRingingActivity.java new file mode 100644 index 0000000..cce553e --- /dev/null +++ b/app/src/main/java/com/example/bem/AlarmRingingActivity.java @@ -0,0 +1,121 @@ +package com.example.bem; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; + +import java.util.Calendar; + +public class AlarmRingingActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + setShowWhenLocked(true); + setTurnScreenOn(true); + } else { + getWindow().addFlags( + WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | + WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON | + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | + WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD + ); + } + + setContentView(R.layout.activity_alarm_ringing); + + String title = getIntent().getStringExtra("alarm_title"); + String time = getIntent().getStringExtra("alarm_time"); + String ringtoneUri = getIntent().getStringExtra("ringtone_uri"); + int alarmId = getIntent().getIntExtra("alarm_id", -1); + + TextView textTitle = findViewById(R.id.textAlarmTitle); + TextView textTime = findViewById(R.id.textAlarmTime); + Button btnSnooze = findViewById(R.id.btnSnooze); + Button btnStop = findViewById(R.id.btnStop); + + if (title != null) { + textTitle.setText(title); + } + if (time != null) { + textTime.setText(time); + } + + btnSnooze.setOnClickListener(v -> { + snoozeAlarm(title, ringtoneUri, alarmId); + stopAlarmService(); + finish(); + }); + + btnStop.setOnClickListener(v -> { + stopAlarmService(); + finish(); + }); + } + + private void snoozeAlarm(String title, String ringtoneUri, int alarmId) { + AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + if (alarmManager == null) return; + + Intent intent = new Intent(this, AlarmReceiver.class); + intent.putExtra("alarm_title", title); + intent.putExtra("alarm_message", "Hora de tomar: " + title); + intent.putExtra("ringtone_uri", ringtoneUri); + + int snoozeId = alarmId >= 0 ? alarmId + 10000 : (int) System.currentTimeMillis(); + + PendingIntent pendingIntent = PendingIntent.getBroadcast( + this, + snoozeId, + intent, + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + ); + + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.MINUTE, 5); + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + calendar.getTimeInMillis(), + pendingIntent + ); + } else { + alarmManager.setExact( + AlarmManager.RTC_WAKEUP, + calendar.getTimeInMillis(), + pendingIntent + ); + } + + android.widget.Toast.makeText(this, "⏰ Alarme adiado por 5 minutos", android.widget.Toast.LENGTH_LONG).show(); + } catch (SecurityException e) { + android.widget.Toast.makeText(this, "Erro ao adiar alarme", android.widget.Toast.LENGTH_SHORT).show(); + } + } + + private void stopAlarmService() { + Intent serviceIntent = new Intent(this, AlarmSoundService.class); + stopService(serviceIntent); + } + + @Override + public void onBackPressed() { + // Não permite voltar sem parar o alarme + } +} + + + + diff --git a/app/src/main/java/com/example/bem/AlarmSoundService.java b/app/src/main/java/com/example/bem/AlarmSoundService.java new file mode 100644 index 0000000..b361c8f --- /dev/null +++ b/app/src/main/java/com/example/bem/AlarmSoundService.java @@ -0,0 +1,201 @@ +package com.example.bem; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.media.AudioAttributes; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Build; +import android.os.IBinder; +import android.os.VibrationEffect; +import android.os.Vibrator; + +import androidx.core.app.NotificationCompat; + +public class AlarmSoundService extends Service { + + private static final String CHANNEL_ID = "alarm_sound_channel"; + private static final int NOTIFICATION_ID = 2001; + private MediaPlayer mediaPlayer; + private Vibrator vibrator; + + @Override + public void onCreate() { + super.onCreate(); + createNotificationChannel(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent != null && "STOP_ALARM".equals(intent.getAction())) { + stopSelf(); + return START_NOT_STICKY; + } + + String title = intent.getStringExtra("alarm_title"); + String ringtoneUriString = intent.getStringExtra("ringtone_uri"); + + startForeground(NOTIFICATION_ID, createNotification(title)); + + playAlarmSound(ringtoneUriString); + vibratePhone(); + + new android.os.Handler().postDelayed(this::stopSelf, 60000); + + return START_NOT_STICKY; + } + + private void playAlarmSound(String ringtoneUriString) { + try { + Uri soundUri; + if (ringtoneUriString != null && !ringtoneUriString.isEmpty()) { + soundUri = Uri.parse(ringtoneUriString); + } else { + soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); + if (soundUri == null) { + soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); + } + } + + mediaPlayer = new MediaPlayer(); + mediaPlayer.setDataSource(this, soundUri); + + AudioAttributes audioAttributes = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_ALARM) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build(); + mediaPlayer.setAudioAttributes(audioAttributes); + + AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + if (audioManager != null) { + int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_ALARM); + audioManager.setStreamVolume(AudioManager.STREAM_ALARM, maxVolume, 0); + } + + mediaPlayer.setLooping(true); + mediaPlayer.setVolume(1.0f, 1.0f); + + mediaPlayer.setOnPreparedListener(mp -> { + mp.start(); + }); + + mediaPlayer.setOnErrorListener((mp, what, extra) -> { + mp.reset(); + return true; + }); + + mediaPlayer.prepareAsync(); + + } catch (Exception e) { + e.printStackTrace(); + try { + Uri defaultUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); + if (defaultUri != null && mediaPlayer != null) { + mediaPlayer.reset(); + mediaPlayer.setDataSource(this, defaultUri); + mediaPlayer.prepare(); + mediaPlayer.start(); + } + } catch (Exception ex) { + ex.printStackTrace(); + } + } + } + + private void vibratePhone() { + vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); + if (vibrator != null && vibrator.hasVibrator()) { + long[] pattern = { + 0, // Início + 400, // Vibra 400ms + 1200, // Pausa 1.2s + 400, // Vibra 400ms + 3000 // Pausa 3s antes de repetir + }; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + vibrator.vibrate(VibrationEffect.createWaveform(pattern, 0)); + } else { + vibrator.vibrate(pattern, 0); + } + } + } + + private Notification createNotification(String title) { + Intent stopIntent = new Intent(this, AlarmSoundService.class); + stopIntent.setAction("STOP_ALARM"); + PendingIntent stopPendingIntent = PendingIntent.getService( + this, + 0, + stopIntent, + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + ); + + Intent openIntent = new Intent(this, MainActivity.class); + openIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + PendingIntent openPendingIntent = PendingIntent.getActivity( + this, + 0, + openIntent, + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + ); + + return new NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(android.R.drawable.ic_lock_idle_alarm) + .setContentTitle("⏰ " + (title != null ? title : "Alarme de Medicamento")) + .setContentText("Hora de tomar o medicamento! Toque para abrir.") + .setPriority(NotificationCompat.PRIORITY_MAX) + .setCategory(NotificationCompat.CATEGORY_ALARM) + .setOngoing(true) + .setAutoCancel(false) + .setContentIntent(openPendingIntent) + .addAction(android.R.drawable.ic_media_pause, "Parar", stopPendingIntent) + .build(); + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + "Serviço de Alarme", + NotificationManager.IMPORTANCE_HIGH + ); + channel.setDescription("Toca o alarme de medicação"); + channel.setSound(null, null); + + NotificationManager manager = getSystemService(NotificationManager.class); + if (manager != null) { + manager.createNotificationChannel(channel); + } + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + + if (mediaPlayer != null) { + if (mediaPlayer.isPlaying()) { + mediaPlayer.stop(); + } + mediaPlayer.release(); + mediaPlayer = null; + } + + if (vibrator != null) { + vibrator.cancel(); + } + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } +} + diff --git a/app/src/main/java/com/example/bem/InviteCodeActivity.java b/app/src/main/java/com/example/bem/InviteCodeActivity.java new file mode 100644 index 0000000..9586f48 --- /dev/null +++ b/app/src/main/java/com/example/bem/InviteCodeActivity.java @@ -0,0 +1,248 @@ +package com.example.bem; + +import android.content.Intent; +import android.os.Bundle; +import android.os.CountDownTimer; +import android.text.TextUtils; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.firestore.FieldValue; +import com.google.firebase.firestore.FirebaseFirestore; + +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +public class InviteCodeActivity extends AppCompatActivity { + + private FirebaseAuth mAuth; + private FirebaseFirestore db; + + private LinearLayout layoutGenerateCode; + private LinearLayout layoutEnterCode; + private TextView textInviteCode; + private TextView textCountdown; + private TextView textModeDescription; + private Button btnGenerateCode; + private EditText inputInviteCode; + private Button btnValidateCode; + private Button btnBack; + + private String currentCode = null; + private CountDownTimer countDownTimer; + private boolean isGuardianMode = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_invite_code); + + mAuth = FirebaseAuth.getInstance(); + db = FirebaseFirestore.getInstance(); + + layoutGenerateCode = findViewById(R.id.layoutGenerateCode); + layoutEnterCode = findViewById(R.id.layoutEnterCode); + textInviteCode = findViewById(R.id.textInviteCode); + textCountdown = findViewById(R.id.textCountdown); + textModeDescription = findViewById(R.id.textModeDescription); + btnGenerateCode = findViewById(R.id.btnGenerateCode); + inputInviteCode = findViewById(R.id.inputInviteCode); + btnValidateCode = findViewById(R.id.btnValidateCode); + btnBack = findViewById(R.id.btnBack); + + isGuardianMode = getIntent().getBooleanExtra("is_guardian", false); + + if (isGuardianMode) { + textModeDescription.setText("Insira o código do utilizador para se vincular como responsável"); + layoutGenerateCode.setVisibility(View.GONE); + layoutEnterCode.setVisibility(View.VISIBLE); + } else { + textModeDescription.setText("Gere um código temporário para vincular responsável"); + layoutGenerateCode.setVisibility(View.VISIBLE); + layoutEnterCode.setVisibility(View.GONE); + generateCode(); + } + + btnGenerateCode.setOnClickListener(v -> generateCode()); + btnValidateCode.setOnClickListener(v -> validateCode()); + btnBack.setOnClickListener(v -> finish()); + } + + private void generateCode() { + if (mAuth.getCurrentUser() == null) { + Toast.makeText(this, "Erro: Não autenticado", Toast.LENGTH_SHORT).show(); + return; + } + + String code = String.format("%06d", new Random().nextInt(999999)); + currentCode = code; + textInviteCode.setText(code); + + String userId = mAuth.getCurrentUser().getUid(); + Map codeData = new HashMap<>(); + codeData.put("code", code); + codeData.put("userId", userId); + codeData.put("createdAt", System.currentTimeMillis()); + codeData.put("expiresAt", System.currentTimeMillis() + 30000); + codeData.put("used", false); + + db.collection("inviteCodes").document(code) + .set(codeData) + .addOnSuccessListener(aVoid -> { + startCountdown(); + Toast.makeText(this, "✓ Código gerado! Válido por 30 segundos", Toast.LENGTH_SHORT).show(); + }) + .addOnFailureListener(e -> { + Toast.makeText(this, "Erro ao gerar código: " + e.getMessage(), Toast.LENGTH_LONG).show(); + }); + } + + private void startCountdown() { + if (countDownTimer != null) { + countDownTimer.cancel(); + } + + countDownTimer = new CountDownTimer(30000, 1000) { + @Override + public void onTick(long millisUntilFinished) { + int seconds = (int) (millisUntilFinished / 1000); + textCountdown.setText("Expira em: " + seconds + "s"); + + if (seconds <= 10) { + textCountdown.setTextColor(getResources().getColor(android.R.color.holo_red_dark)); + } + } + + @Override + public void onFinish() { + textCountdown.setText("Código expirado"); + textInviteCode.setText("------"); + currentCode = null; + + btnGenerateCode.setEnabled(true); + } + }; + + countDownTimer.start(); + btnGenerateCode.setEnabled(false); + } + + private void validateCode() { + String code = inputInviteCode.getText().toString().trim(); + + if (TextUtils.isEmpty(code) || code.length() != 6) { + inputInviteCode.setError("Código deve ter 6 dígitos"); + return; + } + + if (mAuth.getCurrentUser() == null) { + Toast.makeText(this, "Erro: Não autenticado", Toast.LENGTH_SHORT).show(); + return; + } + + btnValidateCode.setEnabled(false); + + db.collection("inviteCodes").document(code) + .get() + .addOnSuccessListener(document -> { + if (!document.exists()) { + btnValidateCode.setEnabled(true); + Toast.makeText(this, "❌ Código inválido", Toast.LENGTH_SHORT).show(); + return; + } + + Boolean used = document.getBoolean("used"); + Long expiresAt = document.getLong("expiresAt"); + String targetUserId = document.getString("userId"); + + if (used != null && used) { + btnValidateCode.setEnabled(true); + Toast.makeText(this, "❌ Código já utilizado", Toast.LENGTH_SHORT).show(); + return; + } + + if (expiresAt != null && System.currentTimeMillis() > expiresAt) { + btnValidateCode.setEnabled(true); + Toast.makeText(this, "❌ Código expirado", Toast.LENGTH_SHORT).show(); + return; + } + + linkGuardianToUser(code, targetUserId); + }) + .addOnFailureListener(e -> { + btnValidateCode.setEnabled(true); + Toast.makeText(this, "Erro: " + e.getMessage(), Toast.LENGTH_LONG).show(); + }); + } + + private void linkGuardianToUser(String code, String targetUserId) { + String guardianId = mAuth.getCurrentUser().getUid(); + + Map linkData = new HashMap<>(); + linkData.put("guardianId", guardianId); + linkData.put("linkedAt", System.currentTimeMillis()); + + // 1. Update the Target User's document (Add guardian to their list) + db.collection("users").document(targetUserId) + .update("guardians", FieldValue.arrayUnion(guardianId)) + .addOnSuccessListener(aVoid -> { + + // 2. Update the Guardian's document (Add target user to their list) + db.collection("users").document(guardianId) + .update("managedUsers", FieldValue.arrayUnion(targetUserId)) + .addOnSuccessListener(aVoid2 -> { + + // 3. Mark code as used + db.collection("inviteCodes").document(code) + .update("used", true) + .addOnSuccessListener(aVoid3 -> { + Toast.makeText(this, "✓ Vinculado com sucesso!", Toast.LENGTH_LONG).show(); + + Intent intent = new Intent(this, MainActivity.class); + intent.setFlags( + Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + finish(); + }) + .addOnFailureListener(e -> { + // Even if marking code fails, the link is done. + // But ideally we should handle this. For now, proceeding. + Intent intent = new Intent(this, MainActivity.class); + intent.setFlags( + Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + finish(); + }); + + }) + .addOnFailureListener(e -> { + // If updating guardian fails, we should probably revert or warn. + // For MVP/Simple app: Log and warn. + btnValidateCode.setEnabled(true); + Toast.makeText(this, "Erro ao atualizar perfil do responsável: " + e.getMessage(), + Toast.LENGTH_LONG).show(); + }); + + }) + .addOnFailureListener(e -> { + btnValidateCode.setEnabled(true); + Toast.makeText(this, "Erro ao vincular: " + e.getMessage(), Toast.LENGTH_LONG).show(); + }); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (countDownTimer != null) { + countDownTimer.cancel(); + } + } +} diff --git a/app/src/main/java/com/example/bem/LoginActivity.java b/app/src/main/java/com/example/bem/LoginActivity.java new file mode 100644 index 0000000..ffa7c67 --- /dev/null +++ b/app/src/main/java/com/example/bem/LoginActivity.java @@ -0,0 +1,340 @@ +package com.example.bem; + +import android.app.AlertDialog; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatDelegate; +import androidx.appcompat.app.AppCompatActivity; + +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseUser; +import com.google.firebase.firestore.FirebaseFirestore; + +import java.util.HashMap; +import java.util.Map; + +public class LoginActivity extends AppCompatActivity { + + private FirebaseAuth mAuth; + private FirebaseFirestore db; + private SharedPreferences prefs; + private static final String PREF_DARK_MODE = "dark_mode"; + + private EditText inputEmail; + private EditText inputPassword; + private EditText inputName; + private EditText inputPhone; + private Button btnLogin; + private TextView textLoginType; + private TextView textSwitchMode; + private TextView textForgotPassword; + private TextView textSwitchToGuardian; + private ProgressBar progressBar; + + private boolean isRegisterMode = false; + private boolean isGuardianMode = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + prefs = getSharedPreferences("app_prefs", MODE_PRIVATE); + applyThemeFromPrefs(); + + mAuth = FirebaseAuth.getInstance(); + db = FirebaseFirestore.getInstance(); + + // Forçar login sempre que abrir a app: faz signOut se já existir sessão + if (mAuth.getCurrentUser() != null) { + mAuth.signOut(); + } + + setContentView(R.layout.activity_login); + + inputEmail = findViewById(R.id.inputEmail); + inputPassword = findViewById(R.id.inputPassword); + inputName = findViewById(R.id.inputName); + inputPhone = findViewById(R.id.inputPhone); + btnLogin = findViewById(R.id.btnLogin); + textLoginType = findViewById(R.id.textLoginType); + textSwitchMode = findViewById(R.id.textSwitchMode); + textForgotPassword = findViewById(R.id.textForgotPassword); + textSwitchToGuardian = findViewById(R.id.textSwitchToGuardian); + + btnLogin.setOnClickListener(v -> handleLogin()); + textSwitchMode.setOnClickListener(v -> toggleMode()); + textForgotPassword.setOnClickListener(v -> showForgotPasswordDialog()); + textSwitchToGuardian.setOnClickListener(v -> switchToGuardianMode()); + } + + private void switchToGuardianMode() { + isGuardianMode = !isGuardianMode; + + if (isGuardianMode) { + textLoginType.setText("Login de Responsável"); + textSwitchToGuardian.setText("👤 Sou Utilizador"); + inputPhone.setVisibility(View.VISIBLE); + inputPhone.setHint("Número de telemóvel"); + } else { + textLoginType.setText("Login de Utilizador"); + textSwitchToGuardian.setText("🔒 Sou Responsável"); + inputPhone.setVisibility(View.GONE); + } + + isRegisterMode = false; + updateUI(); + } + + private void toggleMode() { + isRegisterMode = !isRegisterMode; + updateUI(); + } + + private void updateUI() { + if (isRegisterMode) { + btnLogin.setText("Criar Conta"); + textSwitchMode.setText("Já tem conta? Entrar"); + inputName.setVisibility(View.VISIBLE); + + if (isGuardianMode) { + inputPhone.setVisibility(View.VISIBLE); + } + } else { + btnLogin.setText("Entrar"); + textSwitchMode.setText("Não tem conta? Registar"); + inputName.setVisibility(View.GONE); + + if (!isGuardianMode) { + inputPhone.setVisibility(View.GONE); + } + } + } + + private void handleLogin() { + String email = inputEmail.getText().toString().trim(); + String password = inputPassword.getText().toString().trim(); + String name = inputName.getText().toString().trim(); + String phone = inputPhone.getText().toString().trim(); + + if (TextUtils.isEmpty(email)) { + inputEmail.setError("Insira o email"); + return; + } + + if (!android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()) { + inputEmail.setError("Email inválido"); + return; + } + + if (TextUtils.isEmpty(password)) { + inputPassword.setError("Insira a palavra-passe"); + return; + } + + if (password.length() < 6) { + inputPassword.setError("Mínimo 6 caracteres"); + return; + } + + if (isRegisterMode && TextUtils.isEmpty(name)) { + inputName.setError("Insira o nome"); + return; + } + + btnLogin.setEnabled(false); + + if (isRegisterMode) { + registerUser(email, password, name, phone); + } else { + loginUser(email, password); + } + } + + private void registerUser(String email, String password, String name, String phone) { + mAuth.createUserWithEmailAndPassword(email, password) + .addOnCompleteListener(task -> { + if (task.isSuccessful()) { + FirebaseUser user = mAuth.getCurrentUser(); + if (user != null) { + saveUserData(user.getUid(), email, name, phone, true); + } else { + btnLogin.setEnabled(true); + Toast.makeText(this, "Erro ao obter utilizador. Tente entrar com o email/senha.", + Toast.LENGTH_LONG).show(); + mAuth.signOut(); + } + } else { + btnLogin.setEnabled(true); + String error = task.getException() != null ? task.getException().getMessage() + : "Erro desconhecido"; + Toast.makeText(this, "Erro: " + error, Toast.LENGTH_LONG).show(); + } + }); + } + + private void saveUserData(String uid, String email, String name, String phone, boolean fromRegister) { + Map userData = new HashMap<>(); + userData.put("email", email); + userData.put("name", name); + userData.put("phone", phone != null ? phone : ""); + userData.put("type", isGuardianMode ? "guardian" : "user"); + userData.put("createdAt", System.currentTimeMillis()); + + db.collection("users").document(uid) + .set(userData) + .addOnSuccessListener(aVoid -> { + if (fromRegister) { + Toast.makeText(this, "✓ Conta criada! Faça login para continuar.", Toast.LENGTH_LONG).show(); + mAuth.signOut(); + btnLogin.setEnabled(true); + // Recarrega a tela limpa + Intent intent = getIntent(); + finish(); + startActivity(intent); + } else { + redirectToApp(); + } + }) + .addOnFailureListener(e -> { + btnLogin.setEnabled(true); + Toast.makeText(this, "Erro ao salvar dados: " + e.getMessage(), Toast.LENGTH_LONG).show(); + }); + } + + private void loginUser(String email, String password) { + mAuth.signInWithEmailAndPassword(email, password) + .addOnCompleteListener(task -> { + if (task.isSuccessful()) { + FirebaseUser user = mAuth.getCurrentUser(); + if (user != null) { + checkUserTypeAndRedirect(user.getUid()); + } else { + btnLogin.setEnabled(true); + Toast.makeText(this, "Erro ao iniciar sessão. Tente novamente.", Toast.LENGTH_LONG).show(); + } + } else { + btnLogin.setEnabled(true); + String error = task.getException() != null ? task.getException().getMessage() + : "Credenciais inválidas"; + Toast.makeText(this, "Erro: " + error, Toast.LENGTH_LONG).show(); + } + }); + } + + private void checkUserTypeAndRedirect(String uid) { + db.collection("users").document(uid) + .get() + .addOnSuccessListener(document -> { + if (document.exists()) { + String type = document.getString("type"); + prefs.edit() + .putString("user_type", type != null ? type : "user") + .putString("user_name", document.getString("name")) + .apply(); + + if ("guardian".equals(type)) { + // Verifies if guardian is already linked to a user + java.util.List managedUsers = (java.util.List) document.get("managedUsers"); + + if (managedUsers != null && !managedUsers.isEmpty()) { + redirectToApp(); + } else { + // If not linked, redirect to enter access code + Intent intent = new Intent(LoginActivity.this, InviteCodeActivity.class); + intent.putExtra("is_guardian", true); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + finish(); + } + } else { + redirectToApp(); + } + } else { + // Se o documento não existir (ex.: registo antigo sem salvar), cria um básico + FirebaseUser user = mAuth.getCurrentUser(); + String email = user != null ? user.getEmail() : ""; + String name = (email != null && email.contains("@")) ? email.substring(0, email.indexOf("@")) + : "Utilizador"; + + Map userData = new HashMap<>(); + userData.put("email", email); + userData.put("name", name); + userData.put("phone", ""); + userData.put("type", "user"); + userData.put("createdAt", System.currentTimeMillis()); + + db.collection("users").document(uid) + .set(userData) + .addOnSuccessListener(aVoid -> { + prefs.edit() + .putString("user_type", "user") + .putString("user_name", name) + .apply(); + redirectToApp(); + }) + .addOnFailureListener(e -> { + Toast.makeText(this, "Erro ao criar perfil: " + e.getMessage(), Toast.LENGTH_SHORT) + .show(); + btnLogin.setEnabled(true); + }); + } + }) + .addOnFailureListener(e -> { + Toast.makeText(this, "Erro ao carregar dados", Toast.LENGTH_SHORT).show(); + btnLogin.setEnabled(true); + }); + } + + private void redirectToApp() { + Intent intent = new Intent(this, MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + finish(); + } + + private void showForgotPasswordDialog() { + View dialogView = getLayoutInflater().inflate(android.R.layout.simple_list_item_1, null); + EditText emailInput = new EditText(this); + emailInput.setHint("Email de recuperação"); + emailInput.setInputType(android.text.InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS); + emailInput.setPadding(50, 40, 50, 40); + + new AlertDialog.Builder(this) + .setTitle("Recuperar Palavra-passe") + .setMessage("Insira o email para receber o link de recuperação:") + .setView(emailInput) + .setPositiveButton("Enviar", (dialog, which) -> { + String email = emailInput.getText().toString().trim(); + if (TextUtils.isEmpty(email)) { + Toast.makeText(this, "Insira um email", Toast.LENGTH_SHORT).show(); + return; + } + + mAuth.sendPasswordResetEmail(email) + .addOnSuccessListener(aVoid -> { + Toast.makeText(this, "✓ Email enviado! Verifique a caixa de entrada.", + Toast.LENGTH_LONG).show(); + }) + .addOnFailureListener(e -> { + Toast.makeText(this, "Erro: " + e.getMessage(), Toast.LENGTH_LONG).show(); + }); + }) + .setNegativeButton("Cancelar", null) + .show(); + } + + private void applyThemeFromPrefs() { + boolean dark = getSharedPreferences("app_prefs", MODE_PRIVATE).getBoolean(PREF_DARK_MODE, false); + AppCompatDelegate + .setDefaultNightMode(dark ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO); + } +} diff --git a/app/src/main/java/com/example/bem/MainActivity.java b/app/src/main/java/com/example/bem/MainActivity.java new file mode 100644 index 0000000..7334e4b --- /dev/null +++ b/app/src/main/java/com/example/bem/MainActivity.java @@ -0,0 +1,932 @@ +package com.example.bem; + +import android.Manifest; +import android.app.AlarmManager; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.media.Ringtone; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.Switch; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.activity.EdgeToEdge; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseUser; +import com.google.firebase.firestore.FirebaseFirestore; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + +public class MainActivity extends AppCompatActivity { + + private enum Tab { + ALARMS, + REMINDERS, + GUARDIANS, + CALENDAR + } + + private FrameLayout tabContainer; + private LinearLayout tabAlarms; + private LinearLayout tabReminders; + private LinearLayout tabGuardians; + private LinearLayout tabCalendar; + private ImageView iconAlarms; + private ImageView iconReminders; + private ImageView iconGuardians; + private ImageView iconCalendar; + private TextView textAlarms; + private TextView textReminders; + private TextView textGuardians; + private TextView textCalendar; + + private LayoutInflater inflater; + private View alarmsView; + private View remindersView; + private View guardiansView; + private View calendarView; + private LinearLayout alarmListContainer; + private final List alarms = new ArrayList<>(); + private Tab currentTab = null; + private FirebaseAuth mAuth; + private FirebaseFirestore db; + private SharedPreferences prefs; + private static final String PREF_GUARDIAN_PIN = "guardian_pin"; + private static final String PREF_DARK_MODE = "dark_mode"; + private static final int REQUEST_NOTIFICATION_PERMISSION = 1001; + private static final int REQUEST_RINGTONE_PICKER = 1002; + private int editingAlarmIndex = -1; + private String currentUserId = null; + private boolean isGuardian = false; + + private final ActivityResultLauncher notificationPermissionLauncher = registerForActivityResult( + new ActivityResultContracts.RequestPermission(), isGranted -> { + if (isGranted) { + Toast.makeText(this, "Permissão de notificações concedida!", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(this, "Permissão negada. Alarmes podem não tocar.", Toast.LENGTH_LONG).show(); + } + }); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mAuth = FirebaseAuth.getInstance(); + db = FirebaseFirestore.getInstance(); + + FirebaseUser currentUser = mAuth.getCurrentUser(); + if (currentUser == null) { + redirectToLogin(); + return; + } + + prefs = getSharedPreferences("app_prefs", MODE_PRIVATE); + applyThemeFromPrefs(); + + EdgeToEdge.enable(this); + setContentView(R.layout.activity_main); + inflater = LayoutInflater.from(this); + + currentUserId = currentUser.getUid(); + String userType = prefs.getString("user_type", "user"); + isGuardian = "guardian".equals(userType); + + tabContainer = findViewById(R.id.tabContainer); + tabAlarms = findViewById(R.id.tabAlarms); + tabReminders = findViewById(R.id.tabReminders); + tabGuardians = findViewById(R.id.tabGuardians); + tabCalendar = findViewById(R.id.tabCalendar); + + iconAlarms = findViewById(R.id.iconAlarms); + iconReminders = findViewById(R.id.iconReminders); + iconGuardians = findViewById(R.id.iconGuardians); + iconCalendar = findViewById(R.id.iconCalendar); + + textAlarms = findViewById(R.id.textAlarms); + textReminders = findViewById(R.id.textReminders); + textGuardians = findViewById(R.id.textGuardians); + textCalendar = findViewById(R.id.textCalendar); + + ImageView profileButton = findViewById(R.id.profileButton); + + tabAlarms.setOnClickListener(v -> switchTab(Tab.ALARMS)); + tabReminders.setOnClickListener(v -> switchTab(Tab.REMINDERS)); + tabGuardians.setOnClickListener(v -> handleGuardianTabClick()); + tabCalendar.setOnClickListener(v -> switchTab(Tab.CALENDAR)); + profileButton.setOnClickListener(v -> showProfileMenu()); + + requestNotificationPermissions(); + seedSampleAlarms(); + + applyUserModeLayout(); + } + + private void applyUserModeLayout() { + if (isGuardian) { + tabAlarms.setVisibility(View.GONE); + tabReminders.setVisibility(View.GONE); + tabCalendar.setVisibility(View.VISIBLE); + + // Guardian starts on their panel + switchTab(Tab.GUARDIANS); + } else { + tabAlarms.setVisibility(View.VISIBLE); + tabReminders.setVisibility(View.VISIBLE); + tabCalendar.setVisibility(View.GONE); + + // Regular user starts on Alarms + switchTab(Tab.ALARMS); + } + } + + private void redirectToLogin() { + Intent intent = new Intent(this, LoginActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + finish(); + } + + // ... existing requestNotificationPermissions ... + + private void handleGuardianTabClick() { + // Guardians are always allowed + if (isGuardian) { + switchTab(Tab.GUARDIANS); + } else { + checkIfUserHasGuardians(); + } + } + + // ... existing checkIfUserHasGuardians, showAddGuardianDialog, + // showGuardianOptionsDialog, settings ... + + // ... showSettingsMenu ... + + // ... toggleDarkMode, showUserInfo, logout, showProfileMenu, + // applyThemeFromPrefs, openSupportEmail ... + + private void switchTab(Tab tab) { + if (tab == currentTab) { + return; + } + currentTab = tab; + + View oldContent = tabContainer.getChildCount() > 0 ? tabContainer.getChildAt(0) : null; + if (oldContent != null) { + oldContent.animate() + .alpha(0f) + .setDuration(150) + .withEndAction(() -> { + tabContainer.removeAllViews(); + loadAndShowTab(tab); + }) + .start(); + } else { + loadAndShowTab(tab); + } + } + + private void loadAndShowTab(Tab tab) { + View content; + switch (tab) { + case REMINDERS: + if (remindersView == null) { + remindersView = inflater.inflate(R.layout.layout_tab_reminders, tabContainer, false); + } + content = remindersView; + break; + case GUARDIANS: + if (guardiansView == null) { + guardiansView = inflater.inflate(R.layout.layout_tab_guardians, tabContainer, false); + } + loadGuardianData(guardiansView); + content = guardiansView; + break; + case CALENDAR: + if (calendarView == null) { + calendarView = inflater.inflate(R.layout.layout_tab_calendar, tabContainer, false); + } + setupCalendar(calendarView); + content = calendarView; + break; + case ALARMS: + default: + if (alarmsView == null) { + alarmsView = inflater.inflate(R.layout.layout_tab_alarms, tabContainer, false); + } + // ... setup alarmsView listeners ... + Button addAlarm = alarmsView.findViewById(R.id.btnAddAlarm); + ImageView btnSettings = alarmsView.findViewById(R.id.btnSettings); + alarmListContainer = alarmsView.findViewById(R.id.alarmListContainer); + if (addAlarm != null) { + addAlarm.setOnClickListener(v -> showAddAlarmDialog()); + } + if (btnSettings != null) { + btnSettings.setOnClickListener(v -> showSettingsMenu()); + } + renderAlarms(); + content = alarmsView; + break; + } + + content.setAlpha(0f); + tabContainer.addView(content); + content.animate() + .alpha(1f) + .setDuration(120) + .start(); + + updateTabHighlight(tab); + } + + private void setupCalendar(View view) { + LinearLayout calendarGrid = view.findViewById(R.id.calendarGrid); + TextView monthTitle = view.findViewById(R.id.textMonthTitle); + LinearLayout legendContainer = view.findViewById(R.id.legendContainer); + + calendarGrid.removeAllViews(); + legendContainer.removeAllViews(); + + Calendar calendar = Calendar.getInstance(); + int currentDay = calendar.get(Calendar.DAY_OF_MONTH); + int currentMonth = calendar.get(Calendar.MONTH); + + String[] months = { "Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", + "Outubro", "Novembro", "Dezembro" }; + monthTitle.setText(months[currentMonth] + " " + calendar.get(Calendar.YEAR)); + + // Colors for dots + int[] dotColors = { + ContextCompat.getColor(this, R.color.primary), + ContextCompat.getColor(this, R.color.guardian_purple_end), + ContextCompat.getColor(this, R.color.delete), + 0xFFFFA500, // Orange + 0xFF00C853 // Green + }; + + // Map medications to colors + java.util.Map medColors = new java.util.HashMap<>(); + int colorIndex = 0; + for (AlarmItem alarm : alarms) { + medColors.put(alarm.title, dotColors[colorIndex % dotColors.length]); + colorIndex++; + + // Add to legend + View legendItem = new TextView(this); + ((TextView) legendItem).setText("• " + alarm.title); + ((TextView) legendItem).setTextColor(medColors.get(alarm.title)); + ((TextView) legendItem).setTextSize(14); + ((TextView) legendItem).setPadding(0, 0, 0, 8); + legendContainer.addView(legendItem); + } + + calendar.set(Calendar.DAY_OF_MONTH, 1); + int firstDayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) - 1; // 0=Sunday + int daysInMonth = calendar.getActualMaximum(Calendar.DAY_OF_MONTH); + + SharedPreferences logPrefs = getSharedPreferences("medication_log", MODE_PRIVATE); + java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyyMMdd", java.util.Locale.getDefault()); + + LinearLayout currentWeekRow = null; + + // Empty cells for dias before 1st + currentWeekRow = new LinearLayout(this); + currentWeekRow.setOrientation(LinearLayout.HORIZONTAL); + calendarGrid.addView(currentWeekRow); + + for (int i = 0; i < firstDayOfWeek; i++) { + View empty = new View(this); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, 50, 1.0f); + empty.setLayoutParams(params); + currentWeekRow.addView(empty); + } + + int dayOfWeekCounter = firstDayOfWeek; + + for (int day = 1; day <= daysInMonth; day++) { + if (dayOfWeekCounter == 0) { + currentWeekRow = new LinearLayout(this); + currentWeekRow.setOrientation(LinearLayout.HORIZONTAL); + calendarGrid.addView(currentWeekRow); + } + + View dayCell = inflater.inflate(R.layout.item_calendar_day, currentWeekRow, false); + TextView numText = dayCell.findViewById(R.id.textDayNumber); + LinearLayout dots = dayCell.findViewById(R.id.dotsContainer); + + numText.setText(String.valueOf(day)); + + if (day == currentDay) { + numText.setBackground(ContextCompat.getDrawable(this, R.drawable.bg_calendar_day_selected)); + numText.setTextColor(ContextCompat.getColor(this, R.color.primary)); + } else { + numText.setBackground(null); + } + + // Verify taken meds for this day + calendar.set(Calendar.DAY_OF_MONTH, day); + String dateKey = sdf.format(calendar.getTime()); + + for (AlarmItem alarm : alarms) { + String key = "med_" + alarm.title.replaceAll("\\s+", "_") + "_" + dateKey; + if (logPrefs.getBoolean(key + "_taken", false)) { + // Add dot + View dot = new View(this); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(12, 12); + params.setMargins(2, 0, 2, 0); + dot.setLayoutParams(params); + dot.setBackground(ContextCompat.getDrawable(this, R.drawable.bg_dot)); + dot.getBackground().setTint(medColors.get(alarm.title)); + dots.addView(dot); + } + } + + currentWeekRow.addView(dayCell); + + dayOfWeekCounter++; + if (dayOfWeekCounter >= 7) { + dayOfWeekCounter = 0; + } + } + + // Fill remaining cells + if (dayOfWeekCounter != 0) { + while (dayOfWeekCounter < 7) { + View empty = new View(this); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, 50, 1.0f); + empty.setLayoutParams(params); + currentWeekRow.addView(empty); + dayOfWeekCounter++; + } + } + } + + private void updateTabHighlight(Tab tab) { + resetTab(tabAlarms, iconAlarms, textAlarms); + resetTab(tabReminders, iconReminders, textReminders); + resetTab(tabGuardians, iconGuardians, textGuardians); + resetTab(tabCalendar, iconCalendar, textCalendar); + + switch (tab) { + case ALARMS: + highlightTab(tabAlarms, iconAlarms, textAlarms); + break; + case REMINDERS: + highlightTab(tabReminders, iconReminders, textReminders); + break; + case GUARDIANS: + highlightTab(tabGuardians, iconGuardians, textGuardians); + break; + case CALENDAR: + highlightTab(tabCalendar, iconCalendar, textCalendar); + break; + } + } + + private void highlightTab(LinearLayout layout, ImageView icon, TextView text) { + layout.setBackground(ContextCompat.getDrawable(this, R.drawable.bg_tab_active)); + icon.setColorFilter(ContextCompat.getColor(this, R.color.primary)); + text.setTextColor(ContextCompat.getColor(this, R.color.primary)); + } + + private void resetTab(LinearLayout layout, ImageView icon, TextView text) { + layout.setBackground(null); + int neutral = ContextCompat.getColor(this, R.color.neutral_medium); + icon.setColorFilter(neutral); + text.setTextColor(neutral); + } + + private void seedSampleAlarms() { + alarms.add(new AlarmItem("Tomar medicamentos", "08:00 • 100mg", "Seg • Ter • Qua • Qui • Sex", true)); + alarms.add(new AlarmItem("Almoço", "12:30 • 1 comprimido", "Seg • Ter • Qua • Qui • Sex • Sáb • Dom", true)); + } + + private void renderAlarms() { + if (alarmListContainer == null) { + return; + } + alarmListContainer.removeAllViews(); + if (alarms.isEmpty()) { + TextView empty = new TextView(this); + empty.setText("Sem alarmes. Toque em \"+ Novo\" para criar um."); + empty.setTextColor(ContextCompat.getColor(this, R.color.neutral_medium)); + empty.setPadding(0, 24, 0, 24); + alarmListContainer.addView(empty); + return; + } + for (int i = 0; i < alarms.size(); i++) { + final int index = i; + AlarmItem item = alarms.get(i); + View card = inflater.inflate(R.layout.item_alarm_card, alarmListContainer, false); + TextView title = card.findViewById(R.id.textAlarmTitle); + TextView time = card.findViewById(R.id.textAlarmTime); + TextView days = card.findViewById(R.id.textAlarmDays); + Switch toggle = card.findViewById(R.id.switchAlarm); + ImageButton edit = card.findViewById(R.id.buttonEditAlarm); + ImageButton delete = card.findViewById(R.id.buttonDeleteAlarm); + Button confirmBtn = card.findViewById(R.id.btnConfirmMedication); + + title.setText(item.title); + time.setText(item.time); + days.setText(item.days); + toggle.setChecked(item.enabled); + + if (item.takenToday) { + confirmBtn.setText("✓ Tomado"); + confirmBtn.setBackgroundColor(ContextCompat.getColor(this, R.color.neutral_medium)); + confirmBtn.setEnabled(false); + } else { + confirmBtn.setText("✓ Confirmar"); + confirmBtn.setBackground(ContextCompat.getDrawable(this, R.drawable.bg_new_button)); + confirmBtn.setEnabled(true); + } + + confirmBtn.setOnClickListener(v -> { + item.takenToday = true; + item.lastTakenTimestamp = System.currentTimeMillis(); + saveMedicationConfirmation(item); + renderAlarms(); + showConfirmationDialog(item); + }); + + toggle.setOnCheckedChangeListener((buttonView, isChecked) -> { + item.enabled = isChecked; + Toast.makeText(this, + item.title + (isChecked ? " ativado" : " desativado"), + Toast.LENGTH_SHORT).show(); + }); + + edit.setOnClickListener(v -> showEditAlarmDialog(item, index)); + + delete.setOnClickListener(v -> { + new AlertDialog.Builder(this) + .setTitle("Remover Alarme") + .setMessage("Tem certeza que deseja remover \"" + item.title + "\"?") + .setPositiveButton("Remover", (dialog, which) -> { + alarms.remove(index); + renderAlarms(); + Toast.makeText(this, "Alarme removido", Toast.LENGTH_SHORT).show(); + }) + .setNegativeButton("Cancelar", null) + .show(); + }); + + alarmListContainer.addView(card); + } + } + + private void showConfirmationDialog(AlarmItem item) { + java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("HH:mm", java.util.Locale.getDefault()); + String timeStr = sdf.format(new java.util.Date(item.lastTakenTimestamp)); + + View dialogView = inflater.inflate(R.layout.dialog_add_alarm, null, false); + + new AlertDialog.Builder(this) + .setTitle("✓ Medicamento Confirmado") + .setMessage("Confirmou que tomou:\n\n" + + "💊 " + item.title + "\n" + + "🕐 Às " + timeStr + "\n\n" + + "O responsável será notificado automaticamente.") + .setPositiveButton("OK", null) + .show(); + + Toast.makeText(this, "Responsável notificado!", Toast.LENGTH_LONG).show(); + } + + private void saveMedicationConfirmation(AlarmItem item) { + SharedPreferences confirmPrefs = getSharedPreferences("medication_log", MODE_PRIVATE); + String key = "med_" + item.title.replaceAll("\\s+", "_") + "_" + + new java.text.SimpleDateFormat("yyyyMMdd", java.util.Locale.getDefault()) + .format(new java.util.Date()); + confirmPrefs.edit() + .putLong(key, item.lastTakenTimestamp) + .putBoolean(key + "_taken", true) + .putString(key + "_title", item.title) + .apply(); + } + + private void loadGuardianData(View guardiansView) { + SharedPreferences confirmPrefs = getSharedPreferences("medication_log", MODE_PRIVATE); + java.text.SimpleDateFormat dateFormat = new java.text.SimpleDateFormat("yyyyMMdd", + java.util.Locale.getDefault()); + String today = dateFormat.format(new java.util.Date()); + + int takenCount = 0; + int totalCount = alarms.size(); + + for (AlarmItem alarm : alarms) { + String key = "med_" + alarm.title.replaceAll("\\s+", "_") + "_" + today; + if (confirmPrefs.getBoolean(key + "_taken", false)) { + takenCount++; + } + } + + TextView takenText = guardiansView.findViewById(R.id.textTakenCount); + TextView pendingText = guardiansView.findViewById(R.id.textPendingCount); + TextView adherenceText = guardiansView.findViewById(R.id.textAdherenceRate); + + if (takenText != null) { + takenText.setText(String.valueOf(takenCount)); + } + if (pendingText != null) { + pendingText.setText(String.valueOf(totalCount - takenCount)); + } + if (adherenceText != null) { + int adherence = totalCount > 0 ? (takenCount * 100 / totalCount) : 0; + adherenceText.setText(adherence + "%"); + } + + LinearLayout statusContainer = guardiansView.findViewById(R.id.statusContainer); + if (statusContainer != null) { + statusContainer.removeAllViews(); + + for (AlarmItem alarm : alarms) { + String key = "med_" + alarm.title.replaceAll("\\s+", "_") + "_" + today; + boolean taken = confirmPrefs.getBoolean(key + "_taken", false); + long timestamp = confirmPrefs.getLong(key, 0); + + View statusCard = inflater.inflate(R.layout.item_alarm_card, statusContainer, false); + TextView titleView = statusCard.findViewById(R.id.textAlarmTitle); + TextView timeView = statusCard.findViewById(R.id.textAlarmTime); + TextView daysView = statusCard.findViewById(R.id.textAlarmDays); + Button confirmBtn = statusCard.findViewById(R.id.btnConfirmMedication); + Switch toggleSwitch = statusCard.findViewById(R.id.switchAlarm); + ImageButton deleteBtn = statusCard.findViewById(R.id.buttonDeleteAlarm); + + titleView.setText(alarm.title); + timeView.setText(alarm.time); + + toggleSwitch.setVisibility(View.GONE); + deleteBtn.setVisibility(View.GONE); + confirmBtn.setVisibility(View.GONE); + + if (taken) { + java.text.SimpleDateFormat timeFormat = new java.text.SimpleDateFormat("HH:mm", + java.util.Locale.getDefault()); + String takenTime = timeFormat.format(new java.util.Date(timestamp)); + daysView.setText("✓ Tomado às " + takenTime); + daysView.setTextColor(ContextCompat.getColor(this, R.color.primary)); + daysView.setBackgroundColor(ContextCompat.getColor(this, R.color.accent)); + } else { + daysView.setText("⚠ Pendente"); + daysView.setTextColor(ContextCompat.getColor(this, R.color.delete)); + daysView.setBackgroundColor(ContextCompat.getColor(this, R.color.background_surface)); + } + + statusContainer.addView(statusCard); + } + } + } + + private void showAddAlarmDialog() { + View dialogView = inflater.inflate(R.layout.dialog_add_alarm, null, false); + EditText titleInput = dialogView.findViewById(R.id.inputAlarmTitle); + TextView timeDisplay = dialogView.findViewById(R.id.textSelectedTime); + EditText detailsInput = dialogView.findViewById(R.id.inputAlarmDetails); + + final int[] selectedHour = { 8 }; + final int[] selectedMinute = { 0 }; + + timeDisplay.setText(String.format("%02d:%02d", selectedHour[0], selectedMinute[0])); + timeDisplay.setTextColor(ContextCompat.getColor(this, R.color.neutral_dark)); + + View timePickerLayout = (View) timeDisplay.getParent(); + if (timePickerLayout != null) { + timePickerLayout.setOnClickListener(v -> { + android.app.TimePickerDialog picker = new android.app.TimePickerDialog( + this, + (view, hourOfDay, minute) -> { + selectedHour[0] = hourOfDay; + selectedMinute[0] = minute; + timeDisplay.setText(String.format("%02d:%02d", hourOfDay, minute)); + timeDisplay.setTextColor(ContextCompat.getColor(this, R.color.neutral_dark)); + }, + selectedHour[0], + selectedMinute[0], + true); + picker.setTitle("Escolha o horário"); + picker.show(); + }); + } + + AlertDialog dialog = new AlertDialog.Builder(this) + .setView(dialogView) + .setPositiveButton("Guardar", null) + .setNegativeButton("Cancelar", null) + .create(); + + dialog.setOnShowListener(dialogInterface -> { + Button positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + positiveButton.setTextColor(ContextCompat.getColor(this, R.color.primary)); + positiveButton.setOnClickListener(v -> { + String title = titleInput.getText().toString().trim(); + String details = detailsInput.getText().toString().trim(); + + if (TextUtils.isEmpty(title)) { + titleInput.setError("Insira o nome do medicamento"); + titleInput.requestFocus(); + return; + } + + if (TextUtils.isEmpty(details)) { + details = "Configurar dias na edição"; + } + + String timeFormatted = String.format("%02d:%02d", selectedHour[0], selectedMinute[0]); + AlarmItem newAlarm = new AlarmItem(title, timeFormatted + " • " + details, + "Seg • Ter • Qua • Qui • Sex", true); + newAlarm.hour = selectedHour[0]; + newAlarm.minute = selectedMinute[0]; + alarms.add(newAlarm); + scheduleAlarm(newAlarm, alarms.size() - 1); + renderAlarms(); + Toast.makeText(this, "Alarme criado e agendado!", Toast.LENGTH_SHORT).show(); + dialog.dismiss(); + }); + + Button negativeButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE); + negativeButton.setTextColor(ContextCompat.getColor(this, R.color.neutral_medium)); + }); + + dialog.show(); + } + + private void showEditAlarmDialog(AlarmItem item, int index) { + editingAlarmIndex = index; + + String[] options = { + "⏰ Alterar horário", + "🔔 Escolher toque", + "🎵 Testar toque atual", + "📝 Editar título" + }; + + new AlertDialog.Builder(this) + .setTitle("Editar: " + item.title) + .setItems(options, (dialog, which) -> { + switch (which) { + case 0: + showChangeTimeDialog(item, index); + break; + case 1: + openRingtonePicker(item); + break; + case 2: + testRingtone(item); + break; + case 3: + showEditTitleDialog(item, index); + break; + } + }) + .setNegativeButton("Fechar", null) + .show(); + } + + private void showChangeTimeDialog(AlarmItem item, int index) { + android.app.TimePickerDialog picker = new android.app.TimePickerDialog( + this, + (view, hourOfDay, minute) -> { + item.hour = hourOfDay; + item.minute = minute; + item.time = String.format("%02d:%02d • %s", hourOfDay, minute, + item.time.contains("•") ? item.time.split("•")[1].trim() : ""); + scheduleAlarm(item, index); + renderAlarms(); + Toast.makeText(this, "Horário atualizado!", Toast.LENGTH_SHORT).show(); + }, + item.hour, + item.minute, + true); + picker.setTitle("Novo horário"); + picker.show(); + } + + private void showEditTitleDialog(AlarmItem item, int index) { + EditText input = new EditText(this); + input.setText(item.title); + input.setHint("Nome do medicamento"); + input.setPadding(50, 40, 50, 40); + + new AlertDialog.Builder(this) + .setTitle("Editar nome") + .setView(input) + .setPositiveButton("Guardar", (dialog, which) -> { + String newTitle = input.getText().toString().trim(); + if (!TextUtils.isEmpty(newTitle)) { + item.title = newTitle; + renderAlarms(); + Toast.makeText(this, "Nome atualizado!", Toast.LENGTH_SHORT).show(); + } + }) + .setNegativeButton("Cancelar", null) + .show(); + } + + private void openRingtonePicker(AlarmItem item) { + Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, "Escolher toque para " + item.title); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false); + + Uri currentUri = null; + if (item.ringtoneUri != null && !item.ringtoneUri.isEmpty()) { + try { + currentUri = Uri.parse(item.ringtoneUri); + } catch (Exception e) { + currentUri = null; + } + } + + if (currentUri == null) { + currentUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); + } + + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, currentUri); + + try { + startActivityForResult(intent, REQUEST_RINGTONE_PICKER); + } catch (Exception e) { + Toast.makeText(this, "Erro ao abrir seletor de toques", Toast.LENGTH_SHORT).show(); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == REQUEST_RINGTONE_PICKER && resultCode == RESULT_OK && editingAlarmIndex >= 0) { + if (data == null) { + Toast.makeText(this, "Nenhum toque selecionado", Toast.LENGTH_SHORT).show(); + editingAlarmIndex = -1; + return; + } + + Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); + + if (uri != null && editingAlarmIndex < alarms.size()) { + AlarmItem item = alarms.get(editingAlarmIndex); + item.ringtoneUri = uri.toString(); + + try { + Ringtone ringtone = RingtoneManager.getRingtone(this, uri); + if (ringtone != null) { + item.ringtoneName = ringtone.getTitle(this); + if (item.ringtoneName == null || item.ringtoneName.isEmpty()) { + item.ringtoneName = "Toque personalizado"; + } + } else { + item.ringtoneName = "Toque do sistema"; + } + } catch (Exception e) { + item.ringtoneName = "Toque selecionado"; + } + + scheduleAlarm(item, editingAlarmIndex); + + Toast.makeText(this, "✓ Toque guardado: " + item.ringtoneName + "\nToque em 'Testar' para ouvir", + Toast.LENGTH_LONG).show(); + } else if (uri == null && editingAlarmIndex < alarms.size()) { + AlarmItem item = alarms.get(editingAlarmIndex); + item.ringtoneUri = null; + item.ringtoneName = "Toque padrão do sistema"; + Toast.makeText(this, "Usando toque padrão", Toast.LENGTH_SHORT).show(); + } + editingAlarmIndex = -1; + } + } + + private Ringtone currentlyPlayingRingtone = null; + + private void testRingtone(AlarmItem item) { + if (currentlyPlayingRingtone != null && currentlyPlayingRingtone.isPlaying()) { + currentlyPlayingRingtone.stop(); + currentlyPlayingRingtone = null; + Toast.makeText(this, "Toque parado", Toast.LENGTH_SHORT).show(); + return; + } + + Uri ringtoneUri; + if (item.ringtoneUri != null) { + ringtoneUri = Uri.parse(item.ringtoneUri); + } else { + ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); + } + + try { + currentlyPlayingRingtone = RingtoneManager.getRingtone(this, ringtoneUri); + currentlyPlayingRingtone.play(); + + Toast.makeText(this, "🔊 Tocando: " + item.ringtoneName + "\nToque novamente para parar", Toast.LENGTH_LONG) + .show(); + + new android.os.Handler().postDelayed(() -> { + if (currentlyPlayingRingtone != null && currentlyPlayingRingtone.isPlaying()) { + currentlyPlayingRingtone.stop(); + currentlyPlayingRingtone = null; + } + }, 5000); + } catch (Exception e) { + Toast.makeText(this, "Erro ao reproduzir toque", Toast.LENGTH_SHORT).show(); + } + } + + private void scheduleAlarm(AlarmItem item, int alarmId) { + AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + if (alarmManager == null) + return; + + Intent intent = new Intent(this, AlarmReceiver.class); + intent.putExtra("alarm_title", item.title); + intent.putExtra("alarm_message", "Hora de tomar: " + item.title); + intent.putExtra("ringtone_uri", item.ringtoneUri); + intent.putExtra("alarm_id", alarmId); + + PendingIntent pendingIntent = PendingIntent.getBroadcast( + this, + alarmId, + intent, + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); + + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR_OF_DAY, item.hour); + calendar.set(Calendar.MINUTE, item.minute); + calendar.set(Calendar.SECOND, 0); + + if (calendar.getTimeInMillis() < System.currentTimeMillis()) { + calendar.add(Calendar.DAY_OF_MONTH, 1); + } + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + calendar.getTimeInMillis(), + pendingIntent); + } else { + alarmManager.setExact( + AlarmManager.RTC_WAKEUP, + calendar.getTimeInMillis(), + pendingIntent); + } + Toast.makeText(this, "Alarme programado para " + item.hour + ":" + String.format("%02d", item.minute), + Toast.LENGTH_SHORT).show(); + } catch (SecurityException e) { + Toast.makeText(this, "Erro: Permissão de alarme necessária", Toast.LENGTH_SHORT).show(); + } + } + + private static class AlarmItem { + String title; + String time; + String days; + boolean enabled; + int hour; + int minute; + boolean takenToday; + long lastTakenTimestamp; + String ringtoneUri; + String ringtoneName; + + AlarmItem(String title, String time, String days, boolean enabled) { + this.title = title; + this.time = time; + this.days = days; + this.enabled = enabled; + this.hour = 8; + this.minute = 0; + this.takenToday = false; + this.lastTakenTimestamp = 0; + this.ringtoneUri = null; + this.ringtoneName = "Toque padrão"; + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_alarm_card.xml b/app/src/main/res/drawable/bg_alarm_card.xml new file mode 100644 index 0000000..14132a4 --- /dev/null +++ b/app/src/main/res/drawable/bg_alarm_card.xml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/app/src/main/res/drawable/bg_avatar.xml b/app/src/main/res/drawable/bg_avatar.xml new file mode 100644 index 0000000..13d99d5 --- /dev/null +++ b/app/src/main/res/drawable/bg_avatar.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_bottom_bar.xml b/app/src/main/res/drawable/bg_bottom_bar.xml new file mode 100644 index 0000000..4ce056a --- /dev/null +++ b/app/src/main/res/drawable/bg_bottom_bar.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_calendar_day_selected.xml b/app/src/main/res/drawable/bg_calendar_day_selected.xml new file mode 100644 index 0000000..044c63a --- /dev/null +++ b/app/src/main/res/drawable/bg_calendar_day_selected.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_chip.xml b/app/src/main/res/drawable/bg_chip.xml new file mode 100644 index 0000000..976119e --- /dev/null +++ b/app/src/main/res/drawable/bg_chip.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_day_chip.xml b/app/src/main/res/drawable/bg_day_chip.xml new file mode 100644 index 0000000..dd500c4 --- /dev/null +++ b/app/src/main/res/drawable/bg_day_chip.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_dot.xml b/app/src/main/res/drawable/bg_dot.xml new file mode 100644 index 0000000..5b5b2ed --- /dev/null +++ b/app/src/main/res/drawable/bg_dot.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_guardian_card.xml b/app/src/main/res/drawable/bg_guardian_card.xml new file mode 100644 index 0000000..2a2b3bc --- /dev/null +++ b/app/src/main/res/drawable/bg_guardian_card.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_guardian_chip.xml b/app/src/main/res/drawable/bg_guardian_chip.xml new file mode 100644 index 0000000..d6815e0 --- /dev/null +++ b/app/src/main/res/drawable/bg_guardian_chip.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_guardian_hero.xml b/app/src/main/res/drawable/bg_guardian_hero.xml new file mode 100644 index 0000000..e4f3f32 --- /dev/null +++ b/app/src/main/res/drawable/bg_guardian_hero.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_header_gradient.xml b/app/src/main/res/drawable/bg_header_gradient.xml new file mode 100644 index 0000000..1514839 --- /dev/null +++ b/app/src/main/res/drawable/bg_header_gradient.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_hero_card.xml b/app/src/main/res/drawable/bg_hero_card.xml new file mode 100644 index 0000000..b16ba79 --- /dev/null +++ b/app/src/main/res/drawable/bg_hero_card.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_icon_button.xml b/app/src/main/res/drawable/bg_icon_button.xml new file mode 100644 index 0000000..7f34c07 --- /dev/null +++ b/app/src/main/res/drawable/bg_icon_button.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_new_button.xml b/app/src/main/res/drawable/bg_new_button.xml new file mode 100644 index 0000000..d8d9f22 --- /dev/null +++ b/app/src/main/res/drawable/bg_new_button.xml @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_reminder_card.xml b/app/src/main/res/drawable/bg_reminder_card.xml new file mode 100644 index 0000000..320bf5d --- /dev/null +++ b/app/src/main/res/drawable/bg_reminder_card.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_reminder_hero.xml b/app/src/main/res/drawable/bg_reminder_hero.xml new file mode 100644 index 0000000..b657106 --- /dev/null +++ b/app/src/main/res/drawable/bg_reminder_hero.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_reminder_info_card.xml b/app/src/main/res/drawable/bg_reminder_info_card.xml new file mode 100644 index 0000000..b8ae69e --- /dev/null +++ b/app/src/main/res/drawable/bg_reminder_info_card.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_search.xml b/app/src/main/res/drawable/bg_search.xml new file mode 100644 index 0000000..02d1b61 --- /dev/null +++ b/app/src/main/res/drawable/bg_search.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_service_card.xml b/app/src/main/res/drawable/bg_service_card.xml new file mode 100644 index 0000000..b7ef8b0 --- /dev/null +++ b/app/src/main/res/drawable/bg_service_card.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_service_icon.xml b/app/src/main/res/drawable/bg_service_icon.xml new file mode 100644 index 0000000..d96bf64 --- /dev/null +++ b/app/src/main/res/drawable/bg_service_icon.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_service_icon_alt.xml b/app/src/main/res/drawable/bg_service_icon_alt.xml new file mode 100644 index 0000000..a5864aa --- /dev/null +++ b/app/src/main/res/drawable/bg_service_icon_alt.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_tab_active.xml b/app/src/main/res/drawable/bg_tab_active.xml new file mode 100644 index 0000000..e543855 --- /dev/null +++ b/app/src/main/res/drawable/bg_tab_active.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..ca3826a --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/logo_bem_temp.xml b/app/src/main/res/drawable/logo_bem_temp.xml new file mode 100644 index 0000000..5e10ada --- /dev/null +++ b/app/src/main/res/drawable/logo_bem_temp.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_alarm_ringing.xml b/app/src/main/res/layout/activity_alarm_ringing.xml new file mode 100644 index 0000000..e1c6cfe --- /dev/null +++ b/app/src/main/res/layout/activity_alarm_ringing.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + +