a localizaçao esta funcionando + ou - certo

This commit is contained in:
2026-03-12 16:00:54 +00:00
parent aab826f651
commit 4055051755
5 changed files with 237 additions and 62 deletions

View File

@@ -7,7 +7,7 @@
</SelectionState>
<SelectionState runConfigName="login_activity">
<option name="selectionMode" value="DIALOG" />
<DropdownSelection timestamp="2026-02-03T16:06:11.417598Z">
<DropdownSelection timestamp="2026-03-12T15:46:37.841197Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/230408/.android/avd/Medium_Phone_2.avd" />
@@ -18,12 +18,12 @@
<targets>
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/230408/.android/avd/Medium_Phone_2.avd" />
<DeviceId pluginId="PhysicalDevice" identifier="serial=rogmvsfmmfus95e6" />
</handle>
</Target>
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/230408/.android/avd/Medium_Phone.avd" />
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/230408/.android/avd/Pixel_9_Pro.avd" />
</handle>
</Target>
</targets>

View File

@@ -13,7 +13,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"

View File

@@ -1,5 +1,10 @@
package com.example.pap_findu;
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;
@@ -20,6 +25,10 @@ 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;
@@ -42,38 +51,65 @@ public class LocationService extends Service {
public void onCreate() {
super.onCreate();
// Inicializa o cliente de GPS e a ligação ao Firebase
// Inicializa o cliente de GPS
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
// ATENÇÃO: Substitua "ID_DA_CRIANCA" pelo ID real do utilizador logado no momento
databaseReference = FirebaseDatabase.getInstance().getReference("users/ID_DA_CRIANCA/live_location");
// =================================================================
// AUTOMAÇÃO: Vai buscar o ID do Filho que está logado neste telemóvel
// =================================================================
FirebaseUser currentUser = FirebaseAuth.getInstance().getCurrentUser();
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");
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 1. Inicia a Notificação Persistente (Obrigatório para serviços em background)
// 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...")
// Substitua ic_launcher pelo ícone da sua app (ex: R.drawable.ic_launcher_foreground)
.setSmallIcon(R.mipmap.ic_launcher)
.build();
startForeground(1, notification);
// 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);
}
// 2. Inicia o Rastreamento de GPS
requestLocationUpdates();
// 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();
}
return START_STICKY;
}
// Ignora o aviso de permissão porque vamos pedir a permissão na Activity ANTES de iniciar este serviço
@SuppressWarnings("MissingPermission")
private void requestLocationUpdates() {
// Configura a frequência do GPS (ex: a cada 10 segundos)
// 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) // Só envia se a pessoa se mover 5 metros (poupa bateria)
.setMinUpdateDistanceMeters(5.0f)
.build();
locationCallback = new LocationCallback() {
@@ -81,19 +117,26 @@ public class LocationService extends Service {
public void onLocationResult(@NonNull LocationResult locationResult) {
super.onLocationResult(locationResult);
for (Location location : locationResult.getLocations()) {
// 3. Envia os dados atualizados para o Firebase Realtime Database
// Prepara os dados
Map<String, Object> locationData = new HashMap<>();
locationData.put("latitude", location.getLatitude());
locationData.put("longitude", location.getLongitude());
locationData.put("last_updated", System.currentTimeMillis());
databaseReference.setValue(locationData);
// Verifica se o utilizador não é nulo antes de enviar para o Firebase
if (databaseReference != null) {
databaseReference.setValue(locationData);
}
}
}
};
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper());
// 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) {
e.printStackTrace();
}
}
@Override
@@ -111,7 +154,7 @@ public class LocationService extends Service {
NotificationChannel channel = new NotificationChannel(
"LocationChannel",
"Monitoramento de Localização",
NotificationManager.IMPORTANCE_LOW // Low para não fazer barulho nem vibrar a toda a hora
NotificationManager.IMPORTANCE_LOW // Low para não fazer barulho nem vibrar
);
NotificationManager manager = getSystemService(NotificationManager.class);
if (manager != null) {

View File

@@ -1,21 +1,51 @@
package com.example.pap_findu;
import android.content.Intent; // <-- ADICIONADO
import android.os.Build; // <-- ADICIONADO
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.widget.Toast;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;
import androidx.navigation.fragment.NavHostFragment;
import com.example.pap_findu.databinding.ActivityMainBinding;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
// ========================================================
// 1. GESTOR DE PERMISSÕES (O "Contrato" com o Android)
// ========================================================
private final ActivityResultLauncher<String[]> requestPermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), permissions -> {
Boolean fineLocationGranted = permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false);
Boolean coarseLocationGranted = permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false);
if (fineLocationGranted != null && fineLocationGranted) {
// Permissão precisa concedida, podemos iniciar o serviço!
startLocationService();
} else if (coarseLocationGranted != null && coarseLocationGranted) {
// Permissão aproximada concedida, podemos iniciar o serviço!
startLocationService();
} else {
// O utilizador recusou as permissões. A app não pode iniciar o serviço.
Toast.makeText(this, "Permissão de localização negada. O rastreamento não funcionará.", Toast.LENGTH_LONG).show();
}
});
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -40,16 +70,39 @@ public class MainActivity extends AppCompatActivity {
}
// ========================================================
// CÓDIGO ADICIONADO: INICIAR O RASTREAMENTO EM BACKGROUND
// 2. PEDE PERMISSÕES ANTES DE LIGAR O MOTOR
// ========================================================
Intent serviceIntent = new Intent(this, LocationService.class);
checkAndRequestPermissions();
}
// O Android 8.0 (Oreo) ou superior exige o startForegroundService
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent);
} else {
startService(serviceIntent);
private void checkAndRequestPermissions() {
List<String> permissionsNeeded = new ArrayList<>();
// Verifica a permissão de localização
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
permissionsNeeded.add(Manifest.permission.ACCESS_FINE_LOCATION);
permissionsNeeded.add(Manifest.permission.ACCESS_COARSE_LOCATION);
}
// ========================================================
// Se for Android 13 ou superior, verifica a permissão de notificações (Obrigatório para Foreground Services)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
permissionsNeeded.add(Manifest.permission.POST_NOTIFICATIONS);
}
}
if (!permissionsNeeded.isEmpty()) {
// Se faltam permissões, pede-as ao utilizador! (Isto vai mostrar a janelinha)
requestPermissionLauncher.launch(permissionsNeeded.toArray(new String[0]));
} else {
// Se já tem todas as permissões, arranca logo com o serviço de localização!
startLocationService();
}
}
private void startLocationService() {
Intent serviceIntent = new Intent(this, LocationService.class);
// O ContextCompat resolve automaticamente os problemas de compatibilidade de versões do Android!
ContextCompat.startForegroundService(this, serviceIntent);
}
}

View File

@@ -5,6 +5,7 @@ import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -15,13 +16,24 @@ import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
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.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
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 com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.QuerySnapshot;
public class MapFragment extends Fragment {
public class MapFragment extends Fragment implements OnMapReadyCallback {
private Button btnAddChild;
private View layoutEmptyState;
@@ -29,6 +41,15 @@ public class MapFragment extends Fragment {
private FirebaseFirestore db;
private FirebaseAuth auth;
// Variáveis para o Mapa e Tempo Real
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) {
@@ -37,7 +58,6 @@ public class MapFragment extends Fragment {
db = FirebaseFirestore.getInstance();
auth = FirebaseAuth.getInstance();
// Encontra os IDs no XML
layoutEmptyState = root.findViewById(R.id.layoutEmptyState);
mapContainer = root.findViewById(R.id.mapContainer);
btnAddChild = root.findViewById(R.id.btnAddChild);
@@ -80,11 +100,20 @@ public class MapFragment extends Fragment {
.limit(1)
.get()
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
QuerySnapshot snapshot = task.getResult();
if (snapshot != null && !snapshot.isEmpty()) {
showMapState();
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");
if (currentChildId != null) {
showMapState(); // Se encontrou o ID, carrega o mapa
} else {
Toast.makeText(getContext(), "Erro: Criança encontrada, mas sem ID.", Toast.LENGTH_SHORT).show();
showEmptyState();
}
} else {
@@ -97,13 +126,79 @@ public class MapFragment extends Fragment {
if (layoutEmptyState != null) layoutEmptyState.setVisibility(View.GONE);
if (mapContainer != null) mapContainer.setVisibility(View.VISIBLE);
Fragment existingMap = getChildFragmentManager().findFragmentById(R.id.mapContainer);
if (existingMap == null) {
SupportMapFragment googleMapFragment = SupportMapFragment.newInstance();
SupportMapFragment mapFragment = (SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.mapContainer);
if (mapFragment == null) {
mapFragment = SupportMapFragment.newInstance();
getChildFragmentManager().beginTransaction()
.replace(R.id.mapContainer, googleMapFragment)
.replace(R.id.mapContainer, mapFragment)
.commit();
}
mapFragment.getMapAsync(this);
}
@Override
public void onMapReady(@NonNull GoogleMap googleMap) {
mMap = googleMap;
mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(0, 0), 2f));
// Inicia a escuta APENAS se tivermos encontrado um ID válido
if (currentChildId != null) {
startListeningToChildLocation();
} else {
Toast.makeText(getContext(), "A aguardar ligação à criança...", Toast.LENGTH_SHORT).show();
}
}
private void startListeningToChildLocation() {
// =================================================================
// AUTOMAÇÃO: Usa a variável currentChildId em vez de texto fixo
// =================================================================
locationRef = FirebaseDatabase.getInstance().getReference("users/" + currentChildId + "/live_location");
locationListener = new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot snapshot) {
if (snapshot.exists()) {
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 (childMarker == null) {
MarkerOptions markerOptions = new MarkerOptions()
.position(childPosition)
.title("Criança");
childMarker = mMap.addMarker(markerOptions);
mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(childPosition, 16f));
} else {
childMarker.setPosition(childPosition);
mMap.animateCamera(CameraUpdateFactory.newLatLng(childPosition));
}
}
} 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());
}
};
locationRef.addValueEventListener(locationListener);
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (locationRef != null && locationListener != null) {
locationRef.removeEventListener(locationListener);
}
}
private void showEmptyState() {
@@ -113,40 +208,24 @@ public class MapFragment extends Fragment {
private void openAddChildScreen() {
AddChildBottomSheet bottomSheet = new AddChildBottomSheet();
bottomSheet.setListener(code -> {
// Quando o código é gerado, mostramos o diálogo de escolha
showCodeDialog(code);
});
bottomSheet.setListener(this::showCodeDialog);
bottomSheet.show(getParentFragmentManager(), "AddChildSheet");
}
// --- AQUI ESTÁ A MUDANÇA PRINCIPAL ---
private void showCodeDialog(String code) {
if (getContext() == null) return;
// 1. Copia o código automaticamente para a área de transferência
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();
// 2. Cria o Alerta com as Duas Opções
new AlertDialog.Builder(getContext())
.setTitle("Filho Adicionado!")
.setMessage("O código de acesso é: " + code + "\n\n(Já copiado automaticamente)")
.setCancelable(false) // Impede fechar clicando fora
// OPÇÃO 1: Ir para o Mapa (Atualiza a tela)
.setPositiveButton("Ir para o Mapa", (dialog, which) -> {
checkIfHasChildren(); // <--- Esta função recarrega a tela e mostra o mapa
})
// OPÇÃO 2: Adicionar Outro (Fica na mesma tela)
.setNegativeButton("Adicionar Outro", (dialog, which) -> {
dialog.dismiss(); // Apenas fecha o alerta e continua na tela azul
})
.setCancelable(false)
.setPositiveButton("Ir para o Mapa", (dialog, which) -> checkIfHasChildren())
.setNegativeButton("Adicionar Outro", (dialog, which) -> dialog.dismiss())
.show();
}
}