a localizaçao esta funcionando + ou - certo

This commit is contained in:
2026-03-12 17:16:29 +00:00
parent fba454ce01
commit d6e9320b80
8 changed files with 361 additions and 355 deletions

View File

@@ -18,12 +18,12 @@
<targets> <targets>
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=rogmvsfmmfus95e6" /> <DeviceId pluginId="LocalEmulator" identifier="path=/Users/230408/.android/avd/Pixel_7.avd" />
</handle> </handle>
</Target> </Target>
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/230408/.android/avd/Pixel_9_Pro.avd" /> <DeviceId pluginId="LocalEmulator" identifier="path=/Users/230408/.android/avd/Pixel_7_filho.avd" />
</handle> </handle>
</Target> </Target>
</targets> </targets>

View File

@@ -4,16 +4,23 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<queries>
<package android:name="com.google.android.gms" />
<intent>
<action android:name="com.google.android.geo.API_KEY" />
</intent>
</queries>
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
@@ -22,12 +29,17 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.PAP_FindU"> android:theme="@style/Theme.PAP_FindU"
tools:targetApi="34">
<meta-data <meta-data
android:name="com.google.android.geo.API_KEY" android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyAxen212OKqkfpu1AbWajLGTCTSdRhJWlM" /> android:value="AIzaSyAxen212OKqkfpu1AbWajLGTCTSdRhJWlM" />
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
<service <service
android:name=".LocationService" android:name=".LocationService"
android:enabled="true" android:enabled="true"
@@ -37,9 +49,11 @@
<activity <activity
android:name=".CriarConta" android:name=".CriarConta"
android:exported="false" /> android:exported="false" />
<activity <activity
android:name=".Recuperar_Passe" android:name=".Recuperar_Passe"
android:exported="false" /> android:exported="false" />
<activity <activity
android:name=".ChatActivity" android:name=".ChatActivity"
android:exported="false" /> android:exported="false" />
@@ -56,6 +70,7 @@
<activity <activity
android:name=".AddZoneActivity" android:name=".AddZoneActivity"
android:exported="false" /> android:exported="false" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@@ -64,6 +79,7 @@
<activity <activity
android:name=".EditProfileActivity" android:name=".EditProfileActivity"
android:exported="false" /> android:exported="false" />
<activity <activity
android:name=".SecurityActivity" android:name=".SecurityActivity"
android:exported="false" /> android:exported="false" />

View File

@@ -2,16 +2,24 @@ package com.example.pap_findu;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.View; import android.util.Log;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.example.pap_findu.adapters.ChatAdapter; import com.example.pap_findu.adapters.ChatAdapter;
import com.example.pap_findu.models.ChatMessage; import com.example.pap_findu.models.ChatMessage;
import com.google.firebase.auth.FirebaseAuth;
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 java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
@@ -28,50 +36,101 @@ public class ChatActivity extends AppCompatActivity {
private ChatAdapter adapter; private ChatAdapter adapter;
private List<ChatMessage> messageList; private List<ChatMessage> messageList;
private DatabaseReference chatRef;
private String currentUserId;
private String accessCode;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.chat_activity); setContentView(R.layout.chat_activity);
// 1. Inicializar Firebase e User
currentUserId = FirebaseAuth.getInstance().getUid();
// Recuperamos o código da "sala" (partilhado entre pai e filho)
accessCode = getSharedPreferences("FindU_Prefs", MODE_PRIVATE)
.getString("child_access_code", null);
if (accessCode == null) {
Toast.makeText(this, "Erro: Sala de chat não encontrada", Toast.LENGTH_SHORT).show();
finish();
return;
}
// Caminho no Firebase: chats / 123456 / messages
chatRef = FirebaseDatabase.getInstance().getReference("chats")
.child(accessCode)
.child("messages");
// 2. Ligar Componentes do Teu Layout
recyclerChat = findViewById(R.id.recycler_chat); recyclerChat = findViewById(R.id.recycler_chat);
editChatMessage = findViewById(R.id.edit_chat_message); editChatMessage = findViewById(R.id.edit_chat_message);
btnSend = findViewById(R.id.btnSend); btnSend = findViewById(R.id.btnSend);
btnBack = findViewById(R.id.btnBack); btnBack = findViewById(R.id.btnBack);
// Initialize Message List with some dummy data // 3. Configurar RecyclerView e Adapter
messageList = new ArrayList<>(); messageList = new ArrayList<>();
messageList.add(new ChatMessage("Olá Miguel! Tudo bem?", true, "10:30"));
messageList.add(new ChatMessage("Cheguei bem à escola.", false, "10:32"));
messageList.add(new ChatMessage("Ainda bem! Qualquer coisa avisa.", true, "10:33"));
// Setup Adapter
adapter = new ChatAdapter(messageList); adapter = new ChatAdapter(messageList);
recyclerChat.setLayoutManager(new LinearLayoutManager(this)); recyclerChat.setLayoutManager(new LinearLayoutManager(this));
recyclerChat.setAdapter(adapter); recyclerChat.setAdapter(adapter);
// Scroll to bottom // 4. Lógica do Botão Enviar
recyclerChat.scrollToPosition(messageList.size() - 1);
// Send Button Logic
btnSend.setOnClickListener(v -> { btnSend.setOnClickListener(v -> {
String text = editChatMessage.getText().toString().trim(); String text = editChatMessage.getText().toString().trim();
if (!TextUtils.isEmpty(text)) { if (!TextUtils.isEmpty(text)) {
sendMessage(text); sendMessageToFirebase(text);
} }
}); });
// Back Button Logic // 5. Lógica do Botão Voltar
btnBack.setOnClickListener(v -> finish()); btnBack.setOnClickListener(v -> finish());
// 6. Começar a ouvir mensagens em tempo real
listenForMessages();
} }
private void sendMessage(String text) { private void sendMessageToFirebase(String text) {
String currentTime = new SimpleDateFormat("HH:mm", Locale.getDefault()).format(new Date()); String currentTime = new SimpleDateFormat("HH:mm", Locale.getDefault()).format(new Date());
ChatMessage newMessage = new ChatMessage(text, true, currentTime);
messageList.add(newMessage); // Criamos a mensagem com o ID de quem envia
adapter.notifyItemInserted(messageList.size() - 1); ChatMessage newMessage = new ChatMessage(text, currentUserId, currentTime);
// "Empurramos" para o Firebase (Gera um ID único automático)
chatRef.push().setValue(newMessage)
.addOnSuccessListener(aVoid -> {
editChatMessage.setText(""); // Limpa o campo se correu bem
})
.addOnFailureListener(e -> {
Toast.makeText(this, "Erro ao enviar", Toast.LENGTH_SHORT).show();
});
}
private void listenForMessages() {
chatRef.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot snapshot) {
messageList.clear();
for (DataSnapshot data : snapshot.getChildren()) {
ChatMessage msg = data.getValue(ChatMessage.class);
if (msg != null) {
// Se o senderId for o MEU, o adapter desenha à direita (azul)
msg.setSentByMe(msg.getSenderId().equals(currentUserId));
messageList.add(msg);
}
}
adapter.notifyDataSetChanged();
// Faz scroll automático para a última mensagem
if (messageList.size() > 0) {
recyclerChat.scrollToPosition(messageList.size() - 1); recyclerChat.scrollToPosition(messageList.size() - 1);
}
}
editChatMessage.setText(""); @Override
public void onCancelled(@NonNull DatabaseError error) {
Log.e("ChatActivity", "Erro no Firebase: " + error.getMessage());
}
});
} }
} }

View File

@@ -4,7 +4,6 @@ import android.Manifest;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo; import android.content.pm.ServiceInfo;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationChannel; import android.app.NotificationChannel;
import android.app.NotificationManager; import android.app.NotificationManager;
@@ -14,6 +13,7 @@ import android.location.Location;
import android.os.Build; import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.os.Looper; import android.os.Looper;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -25,10 +25,6 @@ import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult; import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices; import com.google.android.gms.location.LocationServices;
import com.google.android.gms.location.Priority; import com.google.android.gms.location.Priority;
// IMPORTAÇÕES ADICIONADAS PARA A AUTENTICAÇÃO AUTOMÁTICA
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase; import com.google.firebase.database.FirebaseDatabase;
@@ -41,97 +37,62 @@ public class LocationService extends Service {
private LocationCallback locationCallback; private LocationCallback locationCallback;
private DatabaseReference databaseReference; private DatabaseReference databaseReference;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
// Inicializa o cliente de GPS
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this); fusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
// ================================================================= String childCode = getSharedPreferences("FindU_Prefs", MODE_PRIVATE)
// AUTOMAÇÃO: Vai buscar o ID do Filho que está logado neste telemóvel .getString("child_access_code", null);
// =================================================================
FirebaseUser currentUser = FirebaseAuth.getInstance().getCurrentUser();
if (currentUser != null) { if (childCode != null) {
String myUid = currentUser.getUid(); // Pega o ID único gerado pelo Firebase // CAMINHO EXATO: Direto na raiz, como na tua print
databaseReference = FirebaseDatabase.getInstance().getReference(childCode).child("live_location");
// Cria a referência da base de dados usando este ID dinâmico
databaseReference = FirebaseDatabase.getInstance().getReference("users/" + myUid + "/live_location");
} }
} }
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
// 1. Cria o Canal e a Notificação IMEDIATAMENTE
createNotificationChannel(); createNotificationChannel();
Notification notification = new NotificationCompat.Builder(this, "LocationChannel") Notification notification = new NotificationCompat.Builder(this, "LocationChannel")
.setContentTitle("FindU Ativo") .setContentTitle("FindU Ativo")
.setContentText("A monitorizar a localização em tempo real...") .setContentText("A partilhar localização real...")
.setSmallIcon(R.mipmap.ic_launcher) .setSmallIcon(R.mipmap.ic_launcher)
.build(); .build();
// 2. ENVOLVER TUDO NUM TRY-CATCH PARA EVITAR CRASHES NO ANDROID 14
try {
// O Android Q (API 29) e superior permite/exige especificar o tipo de serviço no código
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION); startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION);
} else { } else {
startForeground(1, notification); startForeground(1, notification);
} }
// 3. Verifica permissão ANTES de ligar o motor de GPS
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
// Se tem permissão, arranca com a recolha de coordenadas!
requestLocationUpdates(); requestLocationUpdates();
} else {
// Se por acaso a permissão falhou, desliga-se silenciosamente
stopForeground(true);
stopSelf();
}
} catch (Exception e) {
// Se o sistema operativo forçar uma paragem (muito comum em emuladores), apanhamos o erro aqui!
e.printStackTrace();
stopSelf();
} }
return START_STICKY; return START_STICKY;
} }
@SuppressWarnings("MissingPermission")
private void requestLocationUpdates() { private void requestLocationUpdates() {
// Configura a frequência do GPS (a cada 10 segundos, só se mover 5 metros) // Pedimos atualizações de 2 em 2 segundos
LocationRequest locationRequest = new LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 10000) LocationRequest locationRequest = new LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 2000)
.setMinUpdateDistanceMeters(5.0f) .setMinUpdateDistanceMeters(0)
.build(); .build();
locationCallback = new LocationCallback() { locationCallback = new LocationCallback() {
@Override @Override
public void onLocationResult(@NonNull LocationResult locationResult) { public void onLocationResult(@NonNull LocationResult locationResult) {
super.onLocationResult(locationResult);
for (Location location : locationResult.getLocations()) { for (Location location : locationResult.getLocations()) {
// Prepara os dados // FILTRO CRÍTICO: Se for a latitude da Google (37.42...), IGNORA.
Map<String, Object> locationData = new HashMap<>(); if (Math.abs(location.getLatitude() - 37.4219) > 0.001) {
locationData.put("latitude", location.getLatitude()); updateFirebase(location);
locationData.put("longitude", location.getLongitude()); } else {
locationData.put("last_updated", System.currentTimeMillis()); Log.d("LocationService", "Ignorada localização falsa da Califórnia");
// Verifica se o utilizador não é nulo antes de enviar para o Firebase
if (databaseReference != null) {
databaseReference.setValue(locationData);
} }
} }
} }
}; };
// Outro try-catch de segurança para a comunicação com os serviços do Google (GMS)
try { try {
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper()); fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper());
} catch (SecurityException e) { } catch (SecurityException e) {
@@ -139,27 +100,32 @@ public class LocationService extends Service {
} }
} }
@Override private void updateFirebase(Location location) {
public void onDestroy() { if (databaseReference != null) {
super.onDestroy(); Map<String, Object> data = new HashMap<>();
// Quando o serviço for desligado, para de usar o GPS para poupar bateria data.put("latitude", location.getLatitude());
if (fusedLocationClient != null && locationCallback != null) { data.put("longitude", location.getLongitude());
fusedLocationClient.removeLocationUpdates(locationCallback); data.put("last_updated", System.currentTimeMillis());
databaseReference.setValue(data);
Log.d("LocationService", "Enviada localização REAL: " + location.getLatitude());
} }
} }
// Cria o Canal de Notificação (obrigatório a partir do Android 8.0) @Override
public void onDestroy() {
if (fusedLocationClient != null && locationCallback != null) {
fusedLocationClient.removeLocationUpdates(locationCallback);
}
super.onDestroy();
}
@Nullable @Override public IBinder onBind(Intent intent) { return null; }
private void createNotificationChannel() { private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel( NotificationChannel channel = new NotificationChannel("LocationChannel", "GPS", NotificationManager.IMPORTANCE_LOW);
"LocationChannel", getSystemService(NotificationManager.class).createNotificationChannel(channel);
"Monitoramento de Localização",
NotificationManager.IMPORTANCE_LOW // Low para não fazer barulho nem vibrar
);
NotificationManager manager = getSystemService(NotificationManager.class);
if (manager != null) {
manager.createNotificationChannel(channel);
}
} }
} }
} }

View File

@@ -1,6 +1,7 @@
package com.example.pap_findu; package com.example.pap_findu;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.text.InputType; import android.text.InputType;
import android.text.TextUtils; import android.text.TextUtils;
@@ -25,10 +26,11 @@ import com.google.firebase.Timestamp;
import com.google.firebase.auth.AuthResult; import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser; import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.firestore.DocumentSnapshot;
import com.google.firebase.firestore.FirebaseFirestore; import com.google.firebase.firestore.FirebaseFirestore;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class login_activity extends AppCompatActivity { public class login_activity extends AppCompatActivity {
@@ -37,7 +39,6 @@ public class login_activity extends AppCompatActivity {
private Button btnLogin; private Button btnLogin;
private TextView criarContaTextView; private TextView criarContaTextView;
// --- NOVAS VARIÁVEIS PARA O FILHO ---
private MaterialButton btnChildLogin; private MaterialButton btnChildLogin;
private FirebaseFirestore db; private FirebaseFirestore db;
private FirebaseAuth mAuth; private FirebaseAuth mAuth;
@@ -54,23 +55,16 @@ public class login_activity extends AppCompatActivity {
return insets; return insets;
}); });
// Inicializar Firebase Auth e Firestore
mAuth = FirebaseAuth.getInstance(); mAuth = FirebaseAuth.getInstance();
db = FirebaseFirestore.getInstance(); db = FirebaseFirestore.getInstance();
// Vincular componentes
emailEditText = findViewById(R.id.emailEditText); emailEditText = findViewById(R.id.emailEditText);
passwordEditText = findViewById(R.id.passwordEditText); passwordEditText = findViewById(R.id.passwordEditText);
btnLogin = findViewById(R.id.btnLogin); btnLogin = findViewById(R.id.btnLogin);
criarContaTextView = findViewById(R.id.criarContaTextView); criarContaTextView = findViewById(R.id.criarContaTextView);
// Novo botão do filho (Certifique-se que adicionou no XML com id btnChildLogin)
btnChildLogin = findViewById(R.id.btnChildLogin); btnChildLogin = findViewById(R.id.btnChildLogin);
// --- LÓGICA 1: LOGIN DO PAI (Seu código original) --- btnLogin.setOnClickListener(v -> {
btnLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String email = emailEditText.getText().toString(); String email = emailEditText.getText().toString();
String password = passwordEditText.getText().toString(); String password = passwordEditText.getText().toString();
@@ -80,42 +74,27 @@ public class login_activity extends AppCompatActivity {
} }
mAuth.signInWithEmailAndPassword(email, password) mAuth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener(login_activity.this, new OnCompleteListener<AuthResult>() { .addOnCompleteListener(login_activity.this, task -> {
@Override
public void onComplete(@NonNull Task<AuthResult> task) {
if (task.isSuccessful()) { if (task.isSuccessful()) {
Toast.makeText(login_activity.this, "Login com sucesso.", Toast.LENGTH_SHORT).show(); Toast.makeText(login_activity.this, "Login com sucesso.", Toast.LENGTH_SHORT).show();
goToMainActivity(); goToMainActivity();
} else { } else {
Toast.makeText(login_activity.this, "Falha na autenticação.", Toast.LENGTH_SHORT).show(); Toast.makeText(login_activity.this, "Falha na autenticação.", Toast.LENGTH_SHORT).show();
} }
}
}); });
}
}); });
// --- LÓGICA 2: LOGIN DO FILHO (Novo código) --- btnChildLogin.setOnClickListener(v -> showChildLoginDialog());
btnChildLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showChildLoginDialog();
}
});
// --- LÓGICA 3: CRIAR CONTA --- criarContaTextView.setOnClickListener(v -> {
criarContaTextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(login_activity.this, CriarConta.class); Intent intent = new Intent(login_activity.this, CriarConta.class);
startActivity(intent); startActivity(intent);
}
}); });
} }
@Override @Override
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
// Verifica se já está logado
FirebaseUser currentUser = mAuth.getCurrentUser(); FirebaseUser currentUser = mAuth.getCurrentUser();
if(currentUser != null){ if(currentUser != null){
goToMainActivity(); goToMainActivity();
@@ -124,28 +103,21 @@ public class login_activity extends AppCompatActivity {
private void goToMainActivity() { private void goToMainActivity() {
Intent intent = new Intent(login_activity.this, MainActivity.class); Intent intent = new Intent(login_activity.this, MainActivity.class);
// Limpa a pilha para não voltar ao login com o botão voltar
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent); startActivity(intent);
finish(); finish();
} }
// ==========================================================
// MÉTODOS DE LOGIN DO FILHO
// ==========================================================
private void showChildLoginDialog() { private void showChildLoginDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this); AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Acesso do Filho"); builder.setTitle("Acesso do Filho");
builder.setMessage("Digite o código de 6 dígitos gerado pelo pai:"); builder.setMessage("Digite o código de 6 dígitos gerado pelo pai:");
// Cria uma caixa de texto dentro do alerta
final EditText inputCode = new EditText(this); final EditText inputCode = new EditText(this);
inputCode.setInputType(InputType.TYPE_CLASS_NUMBER); inputCode.setInputType(InputType.TYPE_CLASS_NUMBER);
inputCode.setHint("Ex: 123456"); inputCode.setHint("Ex: 123456");
builder.setView(inputCode); builder.setView(inputCode);
// Botão Entrar do Alerta
builder.setPositiveButton("Entrar", (dialog, which) -> { builder.setPositiveButton("Entrar", (dialog, which) -> {
String code = inputCode.getText().toString().trim(); String code = inputCode.getText().toString().trim();
if (!TextUtils.isEmpty(code)) { if (!TextUtils.isEmpty(code)) {
@@ -160,14 +132,12 @@ public class login_activity extends AppCompatActivity {
} }
private void verifyChildCode(String code) { private void verifyChildCode(String code) {
// Verifica no Firestore se o código existe
db.collection("login_codes").document(code).get() db.collection("login_codes").document(code).get()
.addOnSuccessListener(document -> { .addOnSuccessListener(document -> {
if (document.exists()) { if (document.exists()) {
boolean used = Boolean.TRUE.equals(document.getBoolean("used")); boolean used = Boolean.TRUE.equals(document.getBoolean("used"));
Timestamp expiresAt = document.getTimestamp("expiresAt"); Timestamp expiresAt = document.getTimestamp("expiresAt");
// Validações
if (used) { if (used) {
Toast.makeText(this, "Este código já foi usado.", Toast.LENGTH_LONG).show(); Toast.makeText(this, "Este código já foi usado.", Toast.LENGTH_LONG).show();
return; return;
@@ -179,7 +149,20 @@ public class login_activity extends AppCompatActivity {
// SUCESSO: O código é bom! // SUCESSO: O código é bom!
String parentId = document.getString("parentId"); String parentId = document.getString("parentId");
loginChildAnonymously(code, parentId); String childName = document.getString("childName");
// =======================================================
// ASSOCIAÇÃO AUTOMÁTICA: Cria o registo na coleção 'children'
// =======================================================
Map<String, Object> childData = new HashMap<>();
childData.put("parentId", parentId);
childData.put("accessCode", code);
childData.put("name", childName != null ? childName : "Filho");
childData.put("createdAt", new Timestamp(new Date()));
db.collection("children").document(code).set(childData)
.addOnSuccessListener(aVoid -> loginChildAnonymously(code, parentId))
.addOnFailureListener(e -> Toast.makeText(this, "Erro ao associar filho.", Toast.LENGTH_SHORT).show());
} else { } else {
Toast.makeText(this, "Código inválido.", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "Código inválido.", Toast.LENGTH_SHORT).show();
@@ -189,13 +172,18 @@ public class login_activity extends AppCompatActivity {
} }
private void loginChildAnonymously(String code, String parentId) { private void loginChildAnonymously(String code, String parentId) {
// Faz login anônimo (sem email)
mAuth.signInAnonymously() mAuth.signInAnonymously()
.addOnCompleteListener(this, task -> { .addOnCompleteListener(this, task -> {
if (task.isSuccessful()) { if (task.isSuccessful()) {
// 1. Invalida o código para ninguém usar de novo // Invalida o código no Firestore
db.collection("login_codes").document(code).update("used", true); db.collection("login_codes").document(code).update("used", true);
// Guarda o código para o LocationService usar
getSharedPreferences("FindU_Prefs", MODE_PRIVATE)
.edit()
.putString("child_access_code", code)
.apply();
Toast.makeText(this, "Conectado como Filho!", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "Conectado como Filho!", Toast.LENGTH_SHORT).show();
goToMainActivity(); goToMainActivity();
} else { } else {

View File

@@ -2,24 +2,53 @@ package com.example.pap_findu.models;
public class ChatMessage { public class ChatMessage {
private String message; private String message;
private boolean isSentByMe; private String senderId;
private String timestamp; private String timestamp;
private boolean isSentByMe;
public ChatMessage(String message, boolean isSentByMe, String timestamp) { // 1. Construtor Vazio (OBRIGATÓRIO para o Firebase conseguir ler os dados)
public ChatMessage() {
}
// 2. Construtor para enviar mensagens (usado na ChatActivity)
public ChatMessage(String message, String senderId, String timestamp) {
this.message = message; this.message = message;
this.isSentByMe = isSentByMe; this.senderId = senderId;
this.timestamp = timestamp; this.timestamp = timestamp;
} }
// --- Getters e Setters ---
public String getMessage() { public String getMessage() {
return message; return message;
} }
public void setMessage(String message) {
this.message = message;
}
public String getSenderId() {
return senderId;
}
public void setSenderId(String senderId) {
this.senderId = senderId;
}
public String getTimestamp() {
return timestamp;
}
public void setTimestamp(String timestamp) {
this.timestamp = timestamp;
}
public boolean isSentByMe() { public boolean isSentByMe() {
return isSentByMe; return isSentByMe;
} }
public String getTimestamp() { // Este setter é usado na ChatActivity dentro do loop listenForMessages
return timestamp; public void setSentByMe(boolean sentByMe) {
isSentByMe = sentByMe;
} }
} }

View File

@@ -4,6 +4,7 @@ import android.app.AlertDialog;
import android.content.ClipData; import android.content.ClipData;
import android.content.ClipboardManager; import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -13,16 +14,21 @@ import android.widget.Button;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import com.example.pap_findu.ChatActivity;
import com.example.pap_findu.R; import com.example.pap_findu.R;
import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.OnMapReadyCallback; import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment; import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions; import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser; import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DataSnapshot;
@@ -31,7 +37,6 @@ import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase; import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener; import com.google.firebase.database.ValueEventListener;
import com.google.firebase.firestore.FirebaseFirestore; import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.QuerySnapshot;
public class MapFragment extends Fragment implements OnMapReadyCallback { public class MapFragment extends Fragment implements OnMapReadyCallback {
@@ -41,18 +46,19 @@ public class MapFragment extends Fragment implements OnMapReadyCallback {
private FirebaseFirestore db; private FirebaseFirestore db;
private FirebaseAuth auth; private FirebaseAuth auth;
// Variáveis para o Mapa e Tempo Real private FloatingActionButton btnAbrirChat;
private MaterialButton btnSOS;
private GoogleMap mMap; private GoogleMap mMap;
private Marker childMarker; private Marker childMarker;
private DatabaseReference locationRef; private DatabaseReference locationRef;
private ValueEventListener locationListener; private ValueEventListener locationListener;
// VARIÁVEL ADICIONADA PARA GUARDAR O ID DA CRIANÇA AUTOMATICAMENTE
private String currentChildId = null; private String currentChildId = null;
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable
ViewGroup container, Bundle savedInstanceState) { @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_map, container, false); View root = inflater.inflate(R.layout.fragment_map, container, false);
db = FirebaseFirestore.getInstance(); db = FirebaseFirestore.getInstance();
@@ -61,16 +67,14 @@ public class MapFragment extends Fragment implements OnMapReadyCallback {
layoutEmptyState = root.findViewById(R.id.layoutEmptyState); layoutEmptyState = root.findViewById(R.id.layoutEmptyState);
mapContainer = root.findViewById(R.id.mapContainer); mapContainer = root.findViewById(R.id.mapContainer);
btnAddChild = root.findViewById(R.id.btnAddChild); btnAddChild = root.findViewById(R.id.btnAddChild);
btnAbrirChat = root.findViewById(R.id.btnAbrirChat);
btnSOS = root.findViewById(R.id.btnSOS);
if (btnAbrirChat != null) btnAbrirChat.setVisibility(View.GONE);
if (btnSOS != null) btnSOS.setVisibility(View.GONE);
if (btnAddChild != null) { if (btnAddChild != null) {
btnAddChild.setOnClickListener(v -> { btnAddChild.setOnClickListener(v -> openAddChildScreen());
FirebaseUser user = auth.getCurrentUser();
if (user != null) {
openAddChildScreen();
} else {
Toast.makeText(getContext(), "Erro: Utilizador não autenticado", Toast.LENGTH_SHORT).show();
}
});
} }
checkUserTypeAndShowScreen(); checkUserTypeAndShowScreen();
@@ -83,13 +87,12 @@ public class MapFragment extends Fragment implements OnMapReadyCallback {
if (user == null) return; if (user == null) return;
if (user.isAnonymous()) { if (user.isAnonymous()) {
if (btnAddChild != null) btnAddChild.setVisibility(View.GONE);
showMapState(); showMapState();
return; setupChildButtons();
} } else {
checkIfHasChildren(); checkIfHasChildren();
} }
}
private void checkIfHasChildren() { private void checkIfHasChildren() {
FirebaseUser user = auth.getCurrentUser(); FirebaseUser user = auth.getCurrentUser();
@@ -101,19 +104,14 @@ public class MapFragment extends Fragment implements OnMapReadyCallback {
.get() .get()
.addOnCompleteListener(task -> { .addOnCompleteListener(task -> {
if (task.isSuccessful() && task.getResult() != null && !task.getResult().isEmpty()) { if (task.isSuccessful() && task.getResult() != null && !task.getResult().isEmpty()) {
currentChildId = task.getResult().getDocuments().get(0).getString("accessCode");
// =================================================================
// AUTOMAÇÃO: Encontra a criança e extrai o ID dela!
// =================================================================
var document = task.getResult().getDocuments().get(0);
// MUITO IMPORTANTE: Mude "uid" para o nome do campo na sua base de dados que tem o ID do filho!
currentChildId = document.getString("uid");
if (currentChildId != null) { if (currentChildId != null) {
showMapState(); // Se encontrou o ID, carrega o mapa showMapState();
setupChildButtons();
if (mMap != null) {
startListeningToChildLocation();
}
} else { } else {
Toast.makeText(getContext(), "Erro: Criança encontrada, mas sem ID.", Toast.LENGTH_SHORT).show();
showEmptyState(); showEmptyState();
} }
} else { } else {
@@ -122,83 +120,98 @@ public class MapFragment extends Fragment implements OnMapReadyCallback {
}); });
} }
private void setupChildButtons() {
if (btnAbrirChat != null) {
btnAbrirChat.setVisibility(View.VISIBLE);
btnAbrirChat.setOnClickListener(v -> {
String roomCode = auth.getCurrentUser().isAnonymous() ?
requireContext().getSharedPreferences("FindU_Prefs", Context.MODE_PRIVATE).getString("child_access_code", null) :
currentChildId;
if (roomCode != null) {
requireContext().getSharedPreferences("FindU_Prefs", Context.MODE_PRIVATE).edit().putString("child_access_code", roomCode).apply();
startActivity(new Intent(getActivity(), ChatActivity.class));
}
});
}
if (btnSOS != null) btnSOS.setVisibility(View.VISIBLE);
}
private void showMapState() { private void showMapState() {
if (layoutEmptyState != null) layoutEmptyState.setVisibility(View.GONE); if (layoutEmptyState != null) layoutEmptyState.setVisibility(View.GONE);
if (mapContainer != null) mapContainer.setVisibility(View.VISIBLE); if (mapContainer != null) mapContainer.setVisibility(View.VISIBLE);
SupportMapFragment mapFragment = (SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.mapContainer); SupportMapFragment mapFragment = (SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.mapContainer);
if (mapFragment == null) { if (mapFragment != null) {
mapFragment = SupportMapFragment.newInstance();
getChildFragmentManager().beginTransaction()
.replace(R.id.mapContainer, mapFragment)
.commit();
}
mapFragment.getMapAsync(this); mapFragment.getMapAsync(this);
} }
}
@Override @Override
public void onMapReady(@NonNull GoogleMap googleMap) { public void onMapReady(@NonNull GoogleMap googleMap) {
mMap = googleMap; mMap = googleMap;
FirebaseUser user = auth.getCurrentUser();
mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(0, 0), 2f)); if (user != null && user.isAnonymous()) {
try { mMap.setMyLocationEnabled(true); } catch (SecurityException e) {}
// Inicia a escuta APENAS se tivermos encontrado um ID válido } else {
if (currentChildId != null) { if (currentChildId != null) {
startListeningToChildLocation(); startListeningToChildLocation();
} else { } else {
Toast.makeText(getContext(), "A aguardar ligação à criança...", Toast.LENGTH_SHORT).show(); mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(0, 0), 2f));
}
} }
} }
private void startListeningToChildLocation() { private void startListeningToChildLocation() {
// ================================================================= if (currentChildId == null || mMap == null) return;
// AUTOMAÇÃO: Usa a variável currentChildId em vez de texto fixo
// ================================================================= // CORREÇÃO: Caminho direto na raiz como aparece no teu Firebase
locationRef = FirebaseDatabase.getInstance().getReference("users/" + currentChildId + "/live_location"); locationRef = FirebaseDatabase.getInstance().getReference(currentChildId).child("live_location");
if (locationListener != null) locationRef.removeEventListener(locationListener);
locationListener = new ValueEventListener() { locationListener = new ValueEventListener() {
@Override @Override
public void onDataChange(@NonNull DataSnapshot snapshot) { public void onDataChange(@NonNull DataSnapshot snapshot) {
if (snapshot.exists()) { if (snapshot.exists() && mMap != null) {
Double lat = snapshot.child("latitude").getValue(Double.class); Double lat = snapshot.child("latitude").getValue(Double.class);
Double lng = snapshot.child("longitude").getValue(Double.class); Double lng = snapshot.child("longitude").getValue(Double.class);
if (lat != null && lng != null && mMap != null) { if (lat != null && lng != null) {
LatLng childPosition = new LatLng(lat, lng); LatLng childPos = new LatLng(lat, lng);
// FILTRO: Só atualiza o mapa se a localização NÃO for a da Google HQ (Califórnia)
if (Math.abs(lat - 37.4219) > 0.001) {
if (childMarker == null) { if (childMarker == null) {
MarkerOptions markerOptions = new MarkerOptions() childMarker = mMap.addMarker(new MarkerOptions()
.position(childPosition) .position(childPos)
.title("Criança"); .title("Filho")
.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE)));
childMarker = mMap.addMarker(markerOptions);
mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(childPosition, 16f));
} else { } else {
childMarker.setPosition(childPosition); childMarker.setPosition(childPos);
mMap.animateCamera(CameraUpdateFactory.newLatLng(childPosition));
}
}
} else {
Log.w("MapFragment", "Pasta de localização vazia ou inexistente para o ID: " + currentChildId);
}
} }
@Override // Move a câmara automaticamente para Vila do Conde
public void onCancelled(@NonNull DatabaseError error) { mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(childPos, 17f));
Log.e("MapFragment", "Erro ao ler localização: " + error.getMessage()); Log.d("MapFragment", "Pai recebeu localização real: " + lat);
} else {
Log.d("MapFragment", "Pai ignorou localização de teste (Califórnia)");
}
}
}
}
@Override public void onCancelled(@NonNull DatabaseError error) {
Log.e("MapFragment", "Erro ao ler Firebase: " + error.getMessage());
} }
}; };
locationRef.addValueEventListener(locationListener); locationRef.addValueEventListener(locationListener);
} }
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
if (locationRef != null && locationListener != null) { if (locationRef != null && locationListener != null) locationRef.removeEventListener(locationListener);
locationRef.removeEventListener(locationListener);
}
} }
private void showEmptyState() { private void showEmptyState() {
@@ -213,19 +226,10 @@ public class MapFragment extends Fragment implements OnMapReadyCallback {
} }
private void showCodeDialog(String code) { private void showCodeDialog(String code) {
if (getContext() == null) return; new AlertDialog.Builder(requireContext())
ClipboardManager clipboard = (ClipboardManager) requireContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("Código", code);
clipboard.setPrimaryClip(clip);
Toast.makeText(getContext(), "Código copiado!", Toast.LENGTH_SHORT).show();
new AlertDialog.Builder(getContext())
.setTitle("Filho Adicionado!") .setTitle("Filho Adicionado!")
.setMessage("O código de acesso é: " + code + "\n\n(Já copiado automaticamente)") .setMessage("Código: " + code)
.setCancelable(false) .setPositiveButton("OK", (d, w) -> checkIfHasChildren())
.setPositiveButton("Ir para o Mapa", (dialog, which) -> checkIfHasChildren())
.setNegativeButton("Adicionar Outro", (dialog, which) -> dialog.dismiss())
.show(); .show();
} }
} }

View File

@@ -5,128 +5,72 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#F3F6FB"> tools:context=".ui.map.MapFragment">
<androidx.constraintlayout.widget.ConstraintLayout <LinearLayout
android:id="@+id/layoutEmptyState" android:id="@+id/layoutEmptyState"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:visibility="visible">
<View
android:id="@+id/viewHeader"
android:layout_width="match_parent"
android:layout_height="160dp"
android:background="@drawable/bg_header_rounded"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvAppTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="FindU"
android:textColor="@android:color/white"
android:textSize="22sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.cardview.widget.CardView
android:id="@+id/cardEmptyState"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="30dp"
android:layout_marginBottom="24dp"
app:cardCornerRadius="20dp"
app:cardElevation="4dp"
app:layout_constraintBottom_toTopOf="@+id/btnSOS"
app:layout_constraintTop_toBottomOf="@+id/tvAppTitle">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp">
<View
android:id="@+id/bgIcon"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@drawable/bg_circle_light_blue"
app:layout_constraintBottom_toTopOf="@+id/tvNoChildTitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<ImageView
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@android:drawable/ic_menu_add"
app:layout_constraintBottom_toBottomOf="@+id/bgIcon"
app:layout_constraintEnd_toEndOf="@+id/bgIcon"
app:layout_constraintStart_toStartOf="@+id/bgIcon"
app:layout_constraintTop_toTopOf="@+id/bgIcon"
app:tint="#3D6DFF" />
<TextView
android:id="@+id/tvNoChildTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Nenhum filho adicionado"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/tvNoChildDesc"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/bgIcon" />
<TextView
android:id="@+id/tvNoChildDesc"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center" android:gravity="center"
android:text="Adicione o seu primeiro filho para começar." android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@+id/btnAddChild" android:visibility="gone">
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvNoChildTitle" />
<com.google.android.material.button.MaterialButton <TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ainda não tem filhos associados."
android:textSize="18sp"
android:layout_marginBottom="16dp"/>
<Button
android:id="@+id/btnAddChild" android:id="@+id/btnAddChild"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="60dp" android:layout_height="wrap_content"
android:text="Adicionar Filho" android:text="Adicionar Filho" />
android:textStyle="bold" </LinearLayout>
app:backgroundTint="#3D6DFF"
app:cornerRadius="12dp"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSOS"
android:layout_width="match_parent"
android:layout_height="65dp"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="24dp"
android:text="SOS"
app:backgroundTint="#D32F2F"
app:cornerRadius="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
android:id="@+id/mapContainer" android:id="@+id/mapContainer"
android:layout_width="match_parent" android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_height="match_parent" android:layout_width="0dp"
android:visibility="gone" /> android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btnAbrirChat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:backgroundTint="#4CAF50"
app:tint="@android:color/white"
android:src="@android:drawable/ic_dialog_email"
app:layout_constraintBottom_toTopOf="@+id/btnSOS"
app:layout_constraintEnd_toEndOf="parent"
android:contentDescription="Abrir Chat" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSOS"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
android:text="BOTÃO SOS DE EMERGÊNCIA"
android:textColor="@android:color/white"
android:textSize="16sp"
android:textStyle="bold"
app:backgroundTint="#D32F2F"
app:cornerRadius="12dp"
app:icon="@android:drawable/ic_menu_call"
app:iconGravity="textEnd"
app:iconPadding="16dp"
app:iconTint="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>