diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index 3b4d6bd..944a55f 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -18,12 +18,12 @@ - + - + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9a9057d..dd316b9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,16 +4,23 @@ - + - - + + + + + + + + + + android:theme="@style/Theme.PAP_FindU" + tools:targetApi="34"> + + + + @@ -56,6 +70,7 @@ + + diff --git a/app/src/main/java/com/example/pap_findu/ChatActivity.java b/app/src/main/java/com/example/pap_findu/ChatActivity.java index f9555a2..c971a4c 100644 --- a/app/src/main/java/com/example/pap_findu/ChatActivity.java +++ b/app/src/main/java/com/example/pap_findu/ChatActivity.java @@ -2,16 +2,24 @@ package com.example.pap_findu; import android.os.Bundle; import android.text.TextUtils; -import android.view.View; +import android.util.Log; import android.widget.EditText; import android.widget.ImageButton; +import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.example.pap_findu.adapters.ChatAdapter; 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.util.ArrayList; @@ -28,50 +36,101 @@ public class ChatActivity extends AppCompatActivity { private ChatAdapter adapter; private List messageList; + private DatabaseReference chatRef; + private String currentUserId; + private String accessCode; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); 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); editChatMessage = findViewById(R.id.edit_chat_message); btnSend = findViewById(R.id.btnSend); btnBack = findViewById(R.id.btnBack); - // Initialize Message List with some dummy data + // 3. Configurar RecyclerView e Adapter 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); recyclerChat.setLayoutManager(new LinearLayoutManager(this)); recyclerChat.setAdapter(adapter); - // Scroll to bottom - recyclerChat.scrollToPosition(messageList.size() - 1); - - // Send Button Logic + // 4. Lógica do Botão Enviar btnSend.setOnClickListener(v -> { String text = editChatMessage.getText().toString().trim(); if (!TextUtils.isEmpty(text)) { - sendMessage(text); + sendMessageToFirebase(text); } }); - // Back Button Logic + // 5. Lógica do Botão Voltar 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()); - ChatMessage newMessage = new ChatMessage(text, true, currentTime); - messageList.add(newMessage); - adapter.notifyItemInserted(messageList.size() - 1); - recyclerChat.scrollToPosition(messageList.size() - 1); + // Criamos a mensagem com o ID de quem envia + ChatMessage newMessage = new ChatMessage(text, currentUserId, currentTime); - editChatMessage.setText(""); + // "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); + } + } + + @Override + public void onCancelled(@NonNull DatabaseError error) { + Log.e("ChatActivity", "Erro no Firebase: " + error.getMessage()); + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/pap_findu/LocationService.java b/app/src/main/java/com/example/pap_findu/LocationService.java index fb295b9..5927bb2 100644 --- a/app/src/main/java/com/example/pap_findu/LocationService.java +++ b/app/src/main/java/com/example/pap_findu/LocationService.java @@ -4,7 +4,6 @@ import android.Manifest; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; import androidx.core.content.ContextCompat; - import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; @@ -14,6 +13,7 @@ import android.location.Location; import android.os.Build; import android.os.IBinder; import android.os.Looper; +import android.util.Log; import androidx.annotation.NonNull; 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.LocationServices; 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.FirebaseDatabase; @@ -41,97 +37,62 @@ public class LocationService extends Service { private LocationCallback locationCallback; private DatabaseReference databaseReference; - @Nullable - @Override - public IBinder onBind(Intent intent) { - return null; - } - @Override public void onCreate() { super.onCreate(); - - // Inicializa o cliente de GPS fusedLocationClient = LocationServices.getFusedLocationProviderClient(this); - // ================================================================= - // AUTOMAÇÃO: Vai buscar o ID do Filho que está logado neste telemóvel - // ================================================================= - FirebaseUser currentUser = FirebaseAuth.getInstance().getCurrentUser(); + String childCode = getSharedPreferences("FindU_Prefs", MODE_PRIVATE) + .getString("child_access_code", null); - if (currentUser != null) { - String myUid = currentUser.getUid(); // Pega o ID único gerado pelo Firebase - - // Cria a referência da base de dados usando este ID dinâmico - databaseReference = FirebaseDatabase.getInstance().getReference("users/" + myUid + "/live_location"); + if (childCode != null) { + // CAMINHO EXATO: Direto na raiz, como na tua print + databaseReference = FirebaseDatabase.getInstance().getReference(childCode).child("live_location"); } } @Override public int onStartCommand(Intent intent, int flags, int startId) { - // 1. Cria o Canal e a Notificação IMEDIATAMENTE createNotificationChannel(); Notification notification = new NotificationCompat.Builder(this, "LocationChannel") .setContentTitle("FindU Ativo") - .setContentText("A monitorizar a localização em tempo real...") + .setContentText("A partilhar localização real...") .setSmallIcon(R.mipmap.ic_launcher) .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) { - startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION); - } else { - startForeground(1, notification); - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION); + } else { + 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) { - // Se tem permissão, arranca com a recolha de coordenadas! - 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(); + if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + requestLocationUpdates(); } return START_STICKY; } - @SuppressWarnings("MissingPermission") private void requestLocationUpdates() { - // Configura a frequência do GPS (a cada 10 segundos, só se mover 5 metros) - LocationRequest locationRequest = new LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 10000) - .setMinUpdateDistanceMeters(5.0f) + // Pedimos atualizações de 2 em 2 segundos + LocationRequest locationRequest = new LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 2000) + .setMinUpdateDistanceMeters(0) .build(); locationCallback = new LocationCallback() { @Override public void onLocationResult(@NonNull LocationResult locationResult) { - super.onLocationResult(locationResult); for (Location location : locationResult.getLocations()) { - // Prepara os dados - Map locationData = new HashMap<>(); - locationData.put("latitude", location.getLatitude()); - locationData.put("longitude", location.getLongitude()); - locationData.put("last_updated", System.currentTimeMillis()); - - // Verifica se o utilizador não é nulo antes de enviar para o Firebase - if (databaseReference != null) { - databaseReference.setValue(locationData); + // FILTRO CRÍTICO: Se for a latitude da Google (37.42...), IGNORA. + if (Math.abs(location.getLatitude() - 37.4219) > 0.001) { + updateFirebase(location); + } else { + Log.d("LocationService", "Ignorada localização falsa da Califórnia"); } } } }; - // Outro try-catch de segurança para a comunicação com os serviços do Google (GMS) try { fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper()); } catch (SecurityException e) { @@ -139,27 +100,32 @@ public class LocationService extends Service { } } - @Override - public void onDestroy() { - super.onDestroy(); - // Quando o serviço for desligado, para de usar o GPS para poupar bateria - if (fusedLocationClient != null && locationCallback != null) { - fusedLocationClient.removeLocationUpdates(locationCallback); + private void updateFirebase(Location location) { + if (databaseReference != null) { + Map data = new HashMap<>(); + data.put("latitude", location.getLatitude()); + data.put("longitude", location.getLongitude()); + 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() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationChannel channel = new NotificationChannel( - "LocationChannel", - "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); - } + NotificationChannel channel = new NotificationChannel("LocationChannel", "GPS", NotificationManager.IMPORTANCE_LOW); + getSystemService(NotificationManager.class).createNotificationChannel(channel); } } } \ No newline at end of file diff --git a/app/src/main/java/com/example/pap_findu/login_activity.java b/app/src/main/java/com/example/pap_findu/login_activity.java index 114cfaf..0ee5771 100644 --- a/app/src/main/java/com/example/pap_findu/login_activity.java +++ b/app/src/main/java/com/example/pap_findu/login_activity.java @@ -1,6 +1,7 @@ package com.example.pap_findu; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Bundle; import android.text.InputType; import android.text.TextUtils; @@ -25,10 +26,11 @@ import com.google.firebase.Timestamp; import com.google.firebase.auth.AuthResult; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseUser; -import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.FirebaseFirestore; import java.util.Date; +import java.util.HashMap; +import java.util.Map; public class login_activity extends AppCompatActivity { @@ -37,7 +39,6 @@ public class login_activity extends AppCompatActivity { private Button btnLogin; private TextView criarContaTextView; - // --- NOVAS VARIÁVEIS PARA O FILHO --- private MaterialButton btnChildLogin; private FirebaseFirestore db; private FirebaseAuth mAuth; @@ -54,68 +55,46 @@ public class login_activity extends AppCompatActivity { return insets; }); - // Inicializar Firebase Auth e Firestore mAuth = FirebaseAuth.getInstance(); db = FirebaseFirestore.getInstance(); - // Vincular componentes emailEditText = findViewById(R.id.emailEditText); passwordEditText = findViewById(R.id.passwordEditText); btnLogin = findViewById(R.id.btnLogin); criarContaTextView = findViewById(R.id.criarContaTextView); - - // Novo botão do filho (Certifique-se que adicionou no XML com id btnChildLogin) btnChildLogin = findViewById(R.id.btnChildLogin); - // --- LÓGICA 1: LOGIN DO PAI (Seu código original) --- - btnLogin.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - String email = emailEditText.getText().toString(); - String password = passwordEditText.getText().toString(); + btnLogin.setOnClickListener(v -> { + String email = emailEditText.getText().toString(); + String password = passwordEditText.getText().toString(); - if (email.isEmpty() || password.isEmpty()) { - Toast.makeText(login_activity.this, "Por favor, preencha todos os campos.", Toast.LENGTH_SHORT).show(); - return; - } - - mAuth.signInWithEmailAndPassword(email, password) - .addOnCompleteListener(login_activity.this, new OnCompleteListener() { - @Override - public void onComplete(@NonNull Task task) { - if (task.isSuccessful()) { - Toast.makeText(login_activity.this, "Login com sucesso.", Toast.LENGTH_SHORT).show(); - goToMainActivity(); - } else { - Toast.makeText(login_activity.this, "Falha na autenticação.", Toast.LENGTH_SHORT).show(); - } - } - }); + if (email.isEmpty() || password.isEmpty()) { + Toast.makeText(login_activity.this, "Por favor, preencha todos os campos.", Toast.LENGTH_SHORT).show(); + return; } + + mAuth.signInWithEmailAndPassword(email, password) + .addOnCompleteListener(login_activity.this, task -> { + if (task.isSuccessful()) { + Toast.makeText(login_activity.this, "Login com sucesso.", Toast.LENGTH_SHORT).show(); + goToMainActivity(); + } else { + Toast.makeText(login_activity.this, "Falha na autenticação.", Toast.LENGTH_SHORT).show(); + } + }); }); - // --- LÓGICA 2: LOGIN DO FILHO (Novo código) --- - btnChildLogin.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - showChildLoginDialog(); - } - }); + btnChildLogin.setOnClickListener(v -> showChildLoginDialog()); - // --- LÓGICA 3: CRIAR CONTA --- - criarContaTextView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent intent = new Intent(login_activity.this, CriarConta.class); - startActivity(intent); - } + criarContaTextView.setOnClickListener(v -> { + Intent intent = new Intent(login_activity.this, CriarConta.class); + startActivity(intent); }); } @Override public void onStart() { super.onStart(); - // Verifica se já está logado FirebaseUser currentUser = mAuth.getCurrentUser(); if(currentUser != null){ goToMainActivity(); @@ -124,28 +103,21 @@ public class login_activity extends AppCompatActivity { private void goToMainActivity() { 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); startActivity(intent); finish(); } - // ========================================================== - // MÉTODOS DE LOGIN DO FILHO - // ========================================================== - private void showChildLoginDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("Acesso do Filho"); 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); inputCode.setInputType(InputType.TYPE_CLASS_NUMBER); inputCode.setHint("Ex: 123456"); builder.setView(inputCode); - // Botão Entrar do Alerta builder.setPositiveButton("Entrar", (dialog, which) -> { String code = inputCode.getText().toString().trim(); if (!TextUtils.isEmpty(code)) { @@ -160,14 +132,12 @@ public class login_activity extends AppCompatActivity { } private void verifyChildCode(String code) { - // Verifica no Firestore se o código existe db.collection("login_codes").document(code).get() .addOnSuccessListener(document -> { if (document.exists()) { boolean used = Boolean.TRUE.equals(document.getBoolean("used")); Timestamp expiresAt = document.getTimestamp("expiresAt"); - // Validações if (used) { Toast.makeText(this, "Este código já foi usado.", Toast.LENGTH_LONG).show(); return; @@ -179,7 +149,20 @@ public class login_activity extends AppCompatActivity { // SUCESSO: O código é bom! 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 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 { 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) { - // Faz login anônimo (sem email) mAuth.signInAnonymously() .addOnCompleteListener(this, task -> { 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); + // 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(); goToMainActivity(); } else { diff --git a/app/src/main/java/com/example/pap_findu/models/ChatMessage.java b/app/src/main/java/com/example/pap_findu/models/ChatMessage.java index 724a799..9d81cee 100644 --- a/app/src/main/java/com/example/pap_findu/models/ChatMessage.java +++ b/app/src/main/java/com/example/pap_findu/models/ChatMessage.java @@ -2,24 +2,53 @@ package com.example.pap_findu.models; public class ChatMessage { private String message; - private boolean isSentByMe; + private String senderId; 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.isSentByMe = isSentByMe; + this.senderId = senderId; this.timestamp = timestamp; } + // --- Getters e Setters --- + public String getMessage() { 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() { return isSentByMe; } - public String getTimestamp() { - return timestamp; + // Este setter é usado na ChatActivity dentro do loop listenForMessages + public void setSentByMe(boolean sentByMe) { + isSentByMe = sentByMe; } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/pap_findu/ui/map/MapFragment.java b/app/src/main/java/com/example/pap_findu/ui/map/MapFragment.java index 22a387f..d787c8d 100644 --- a/app/src/main/java/com/example/pap_findu/ui/map/MapFragment.java +++ b/app/src/main/java/com/example/pap_findu/ui/map/MapFragment.java @@ -4,6 +4,7 @@ import android.app.AlertDialog; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; +import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; @@ -13,16 +14,21 @@ import android.widget.Button; import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import com.example.pap_findu.ChatActivity; import com.example.pap_findu.R; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.OnMapReadyCallback; 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.Marker; 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.FirebaseUser; 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.ValueEventListener; import com.google.firebase.firestore.FirebaseFirestore; -import com.google.firebase.firestore.QuerySnapshot; public class MapFragment extends Fragment implements OnMapReadyCallback { @@ -41,18 +46,19 @@ public class MapFragment extends Fragment implements OnMapReadyCallback { private FirebaseFirestore db; private FirebaseAuth auth; - // Variáveis para o Mapa e Tempo Real + private FloatingActionButton btnAbrirChat; + private MaterialButton btnSOS; + private GoogleMap mMap; private Marker childMarker; private DatabaseReference locationRef; private ValueEventListener locationListener; - // VARIÁVEL ADICIONADA PARA GUARDAR O ID DA CRIANÇA AUTOMATICAMENTE private String currentChildId = null; - public View onCreateView(@NonNull LayoutInflater inflater, - ViewGroup container, Bundle savedInstanceState) { - + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_map, container, false); db = FirebaseFirestore.getInstance(); @@ -61,16 +67,14 @@ public class MapFragment extends Fragment implements OnMapReadyCallback { layoutEmptyState = root.findViewById(R.id.layoutEmptyState); mapContainer = root.findViewById(R.id.mapContainer); 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) { - btnAddChild.setOnClickListener(v -> { - FirebaseUser user = auth.getCurrentUser(); - if (user != null) { - openAddChildScreen(); - } else { - Toast.makeText(getContext(), "Erro: Utilizador não autenticado", Toast.LENGTH_SHORT).show(); - } - }); + btnAddChild.setOnClickListener(v -> openAddChildScreen()); } checkUserTypeAndShowScreen(); @@ -83,12 +87,11 @@ public class MapFragment extends Fragment implements OnMapReadyCallback { if (user == null) return; if (user.isAnonymous()) { - if (btnAddChild != null) btnAddChild.setVisibility(View.GONE); showMapState(); - return; + setupChildButtons(); + } else { + checkIfHasChildren(); } - - checkIfHasChildren(); } private void checkIfHasChildren() { @@ -101,19 +104,14 @@ public class MapFragment extends Fragment implements OnMapReadyCallback { .get() .addOnCompleteListener(task -> { if (task.isSuccessful() && task.getResult() != null && !task.getResult().isEmpty()) { - - // ================================================================= - // 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"); - + currentChildId = task.getResult().getDocuments().get(0).getString("accessCode"); if (currentChildId != null) { - showMapState(); // Se encontrou o ID, carrega o mapa + showMapState(); + setupChildButtons(); + if (mMap != null) { + startListeningToChildLocation(); + } } else { - Toast.makeText(getContext(), "Erro: Criança encontrada, mas sem ID.", Toast.LENGTH_SHORT).show(); showEmptyState(); } } 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() { if (layoutEmptyState != null) layoutEmptyState.setVisibility(View.GONE); if (mapContainer != null) mapContainer.setVisibility(View.VISIBLE); SupportMapFragment mapFragment = (SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.mapContainer); - if (mapFragment == null) { - mapFragment = SupportMapFragment.newInstance(); - getChildFragmentManager().beginTransaction() - .replace(R.id.mapContainer, mapFragment) - .commit(); + if (mapFragment != null) { + mapFragment.getMapAsync(this); } - - mapFragment.getMapAsync(this); } @Override public void onMapReady(@NonNull GoogleMap googleMap) { mMap = googleMap; + FirebaseUser user = auth.getCurrentUser(); - mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(0, 0), 2f)); - - // Inicia a escuta APENAS se tivermos encontrado um ID válido - if (currentChildId != null) { - startListeningToChildLocation(); + if (user != null && user.isAnonymous()) { + try { mMap.setMyLocationEnabled(true); } catch (SecurityException e) {} } else { - Toast.makeText(getContext(), "A aguardar ligação à criança...", Toast.LENGTH_SHORT).show(); + if (currentChildId != null) { + startListeningToChildLocation(); + } else { + mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(0, 0), 2f)); + } } } private void startListeningToChildLocation() { - // ================================================================= - // AUTOMAÇÃO: Usa a variável currentChildId em vez de texto fixo - // ================================================================= - locationRef = FirebaseDatabase.getInstance().getReference("users/" + currentChildId + "/live_location"); + if (currentChildId == null || mMap == null) return; + + // CORREÇÃO: Caminho direto na raiz como aparece no teu Firebase + locationRef = FirebaseDatabase.getInstance().getReference(currentChildId).child("live_location"); + + if (locationListener != null) locationRef.removeEventListener(locationListener); locationListener = new ValueEventListener() { @Override public void onDataChange(@NonNull DataSnapshot snapshot) { - if (snapshot.exists()) { + if (snapshot.exists() && mMap != null) { Double lat = snapshot.child("latitude").getValue(Double.class); Double lng = snapshot.child("longitude").getValue(Double.class); - if (lat != null && lng != null && mMap != null) { - LatLng childPosition = new LatLng(lat, lng); + if (lat != null && lng != null) { + LatLng childPos = new LatLng(lat, lng); - if (childMarker == null) { - MarkerOptions markerOptions = new MarkerOptions() - .position(childPosition) - .title("Criança"); + // 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) { + childMarker = mMap.addMarker(new MarkerOptions() + .position(childPos) + .title("Filho") + .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE))); + } else { + childMarker.setPosition(childPos); + } - childMarker = mMap.addMarker(markerOptions); - mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(childPosition, 16f)); + // Move a câmara automaticamente para Vila do Conde + mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(childPos, 17f)); + Log.d("MapFragment", "Pai recebeu localização real: " + lat); } else { - childMarker.setPosition(childPosition); - mMap.animateCamera(CameraUpdateFactory.newLatLng(childPosition)); + Log.d("MapFragment", "Pai ignorou localização de teste (Califórnia)"); } } - } else { - Log.w("MapFragment", "Pasta de localização vazia ou inexistente para o ID: " + currentChildId); } } - - @Override - public void onCancelled(@NonNull DatabaseError error) { - Log.e("MapFragment", "Erro ao ler localização: " + error.getMessage()); + @Override public void onCancelled(@NonNull DatabaseError error) { + Log.e("MapFragment", "Erro ao ler Firebase: " + error.getMessage()); } }; - locationRef.addValueEventListener(locationListener); } @Override public void onDestroyView() { super.onDestroyView(); - if (locationRef != null && locationListener != null) { - locationRef.removeEventListener(locationListener); - } + if (locationRef != null && locationListener != null) locationRef.removeEventListener(locationListener); } private void showEmptyState() { @@ -213,19 +226,10 @@ public class MapFragment extends Fragment implements OnMapReadyCallback { } private void showCodeDialog(String code) { - if (getContext() == null) return; - - 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()) + new AlertDialog.Builder(requireContext()) .setTitle("Filho Adicionado!") - .setMessage("O código de acesso é: " + code + "\n\n(Já copiado automaticamente)") - .setCancelable(false) - .setPositiveButton("Ir para o Mapa", (dialog, which) -> checkIfHasChildren()) - .setNegativeButton("Adicionar Outro", (dialog, which) -> dialog.dismiss()) + .setMessage("Código: " + code) + .setPositiveButton("OK", (d, w) -> checkIfHasChildren()) .show(); } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_map.xml b/app/src/main/res/layout/fragment_map.xml index 75bfdd8..d3b92bf 100644 --- a/app/src/main/res/layout/fragment_map.xml +++ b/app/src/main/res/layout/fragment_map.xml @@ -5,128 +5,72 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="#F3F6FB"> + tools:context=".ui.map.MapFragment"> - - - + android:gravity="center" + android:orientation="vertical" + android:visibility="gone"> + android:text="Ainda não tem filhos associados." + android:textSize="18sp" + android:layout_marginBottom="16dp"/> - - - - - - - - - - - - - - - - - - - - +