This commit is contained in:
2026-04-21 17:15:25 +01:00
parent d112cd50d7
commit a74f0e4260
16 changed files with 445 additions and 31 deletions

View File

@@ -33,6 +33,13 @@ import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.auth.GoogleAuthProvider;
import com.google.firebase.auth.GoogleAuthProvider;
/**
* CriarContaActivity é a atividade responsável por gerir o registo de novos utilizadores na aplicação LifeGrid.
* Permite a criação de conta utilizando Email/Password e validações de formulário.
* Também suporta registo integrado via conta Google.
*/
public class CriarContaActivity extends AppCompatActivity {
private EditText nomeEditText;
@@ -68,9 +75,14 @@ public class CriarContaActivity extends AppCompatActivity {
firebaseAuth = FirebaseAuth.getInstance();
credentialManager = CredentialManager.create(this);
// Configura o botão de registo para acionar a validação e criação de conta
loginButton2.setOnClickListener(v -> criarConta());
}
/**
* Extrai os dados dos campos de texto, chama o método de validação,
* e caso os dados estejam corretos, regista o utilizador no Firebase Auth.
*/
private void criarConta() {
String nome = nomeEditText.getText().toString().trim();
String email = emailEditText2.getText().toString().trim();
@@ -119,6 +131,10 @@ public class CriarContaActivity extends AppCompatActivity {
});
}
/**
* Valida os diferentes campos do formulário para garantir que estão preenchidos
* corretamente (tamanho e formato do email) antes de tentar comunicar com o Firebase.
*/
private boolean validarDados(String nome, String email, String password, String confirmarPassword) {
// Validar nome
if (TextUtils.isEmpty(nome)) {

View File

@@ -45,6 +45,11 @@ import com.google.firebase.auth.GoogleAuthProvider;
import java.util.concurrent.Executors;
/**
* LoginActivity é o ecrã inicial da aplicação onde o utilizador fornece as suas credenciais.
* Suporta o login regular com Email e Palavra-Passe ligando ao Firebase Auth.
* Também suporta autenticação moderna pela API do CredentialManager (Google Sign In).
*/
public class LoginActivity extends AppCompatActivity {
private TextView textView5;
@@ -106,7 +111,7 @@ public class LoginActivity extends AppCompatActivity {
}
});
// Inicialmente desabilitar o TextView de recuperar senha
// Inicialmente desabilitar o TextView de recuperar palavra-passe
passesquecerTextView.setEnabled(false);
passesquecerTextView.setAlpha(0.5f); // Visualmente mais claro quando desabilitado
@@ -115,11 +120,11 @@ public class LoginActivity extends AppCompatActivity {
public void onClick(View view) {
String email = emailEditText.getText().toString().trim();
// Validar se email antes de abrir a atividade de recuperação
// Validar se tem e-mail antes de abrir a atividade de recuperação
if (TextUtils.isEmpty(email)) {
emailEditText.setError("Por favor, digite seu email primeiro.");
emailEditText.setError("Por favor, digite o seu e-mail primeiro.");
emailEditText.requestFocus();
Toast.makeText(LoginActivity.this, "Digite seu email para recuperar a senha.", Toast.LENGTH_SHORT).show();
Toast.makeText(LoginActivity.this, "Digite o seu e-mail para recuperar a palavra-passe.", Toast.LENGTH_SHORT).show();
return;
}
@@ -147,7 +152,10 @@ public class LoginActivity extends AppCompatActivity {
});
}
/**
* Extrai os dados dos campos de Email e Password, executa as funções de validação
* e caso os dados estejam em boa forma efetua a requisição de entrada pela plataforma Firebase.
*/
private void validarLogin() {
String email = emailEditText.getText().toString().trim();
String password = passwordEditText.getText().toString();
@@ -269,19 +277,19 @@ public class LoginActivity extends AppCompatActivity {
private void launchCredentialManager() {
// [START create_credential_manager_request]
// Instantiate a Google sign-in request
// Instancia um pedido de início de sessão do Google
GetGoogleIdOption googleIdOption = new GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(true)
.setServerClientId(getString(R.string.default_web_client_id))
.build();
// Create the Credential Manager request
// Cria o pedido do Gestor de Credenciais
GetCredentialRequest request = new GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build();
// [END create_credential_manager_request]
// Launch Credential Manager UI
// Lança a interface do Gestor de Credenciais
credentialManager.getCredentialAsync(
getBaseContext(),
request,
@@ -290,7 +298,7 @@ public class LoginActivity extends AppCompatActivity {
new CredentialManagerCallback<>() {
@Override
public void onResult(GetCredentialResponse result) {
// Extract credential from the result returned by Credential Manager
// Extrai a credencial do resultado devolvido pelo Gestor de Credenciais
handleSignIn(result.getCredential());
}
@@ -303,14 +311,14 @@ public class LoginActivity extends AppCompatActivity {
}
private void handleSignIn(Credential credential) {
// Check if credential is of type Google ID
// Verifica se a credencial é do tipo Google ID
if (credential instanceof CustomCredential customCredential
&& credential.getType().equals(GOOGLE_ID_TOKEN_CREDENTIAL)) {
// Create Google ID Token
// Cria o token do Google ID
Bundle credentialData = customCredential.getData();
GoogleIdTokenCredential googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credentialData);
// Sign in to Firebase with using the token
// Inicia a sessão no Firebase usando o token
firebaseAuthWithGoogle(googleIdTokenCredential.getIdToken());
} else {
Log.w(TAG, "Credential is not of type Google ID!");
@@ -318,10 +326,10 @@ public class LoginActivity extends AppCompatActivity {
}
private void signOut() {
// Firebase sign out
// Termina a sessão no Firebase
mAuth.signOut();
// When a user signs out, clear the current user credential state from all credential providers.
// Quando o utilizador termina a sessão, limpa o estado atual da credencial do utilizador de todos os provedores de credenciais.
ClearCredentialStateRequest clearRequest = new ClearCredentialStateRequest();
credentialManager.clearCredentialStateAsync(
clearRequest,
@@ -343,7 +351,7 @@ public class LoginActivity extends AppCompatActivity {
@Override
public void onStart() {
super.onStart();
// Check if user is signed in (non-null) and update UI accordingly.
// Verifica se o utilizador tem sessão iniciada (não nulo) e atualiza a interface conformemente.
FirebaseUser currentUser = mAuth.getCurrentUser();
if (currentUser != null) {
Intent intent = new Intent(LoginActivity.this, TelaInicialActivity.class);

View File

@@ -18,6 +18,10 @@ import androidx.core.view.WindowInsetsCompat;
import com.google.firebase.FirebaseApp;
import com.google.firebase.auth.FirebaseAuth;
/**
* RecupearPasswordActivity é a atividade que coordena o envio do formulário de recuperação de senha.
* Liga-se ao Firebase e envia automaticamente um link de redifinição de palavra-passe para a caixa de correio do utilizador.
*/
public class RecupearPasswordActivity extends AppCompatActivity {
private EditText emailEditText4;
@@ -56,6 +60,10 @@ public class RecupearPasswordActivity extends AppCompatActivity {
loginButton3.setOnClickListener(v -> recuperarPassword());
}
/**
* Valida o campo do e-mail fornecido pelo utilizador para enviar o pedido
* de mudança de palavra-passe aos servidores do serviço Firebase. Trata as respetivas mensagens de sucesso e erro.
*/
private void recuperarPassword() {
String email = emailEditText4.getText().toString().trim();
String emailPassado = getIntent().getStringExtra("email");

View File

@@ -18,6 +18,11 @@ import com.example.lifegrid.menu.GraficosFragment;
import com.example.lifegrid.menu.MetasFragment;
import com.example.lifegrid.menu.TransacoesFragment;
/**
* TelaInicialActivity funciona como a janela mestre (Container) de navegação da aplicação.
* É a responsável por carregar e trocar as visualizações menores (Fragmentos) na área principal do ecrã
* mediante o clique numa série de botões situados na barra de fundo.
*/
public class TelaInicialActivity extends AppCompatActivity {
private TextView tvValor;

View File

@@ -10,11 +10,14 @@ import android.view.ViewGroup;
import com.example.lifegrid.R;
/**
* AtivosFragment diz respeito à janela do ecossistema dedicada à gestão
* do património estático ou em avaliação progressiva na bolsa/bancos.
*/
public class AtivosFragment extends Fragment {
public AtivosFragment() {
// Required empty public constructor
// Construtor público vazio obrigatório
}
@Override
@@ -25,7 +28,7 @@ public class AtivosFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
// Inflaciona o layout para este fragmento
View root = inflater.inflate(R.layout.fragment_ativos, container, false);

View File

@@ -10,11 +10,14 @@ import android.view.ViewGroup;
import com.example.lifegrid.R;
/**
* DefinicoesFragment carrega os menus base de configurações do utilizador,
* sendo o local ideal para definir opções de acessibilidade, mudança de password e logout.
*/
public class DefinicoesFragment extends Fragment {
public DefinicoesFragment() {
// Required empty public constructor
// Construtor público vazio obrigatório
}
@Override
@@ -25,7 +28,7 @@ public class DefinicoesFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
// Inflaciona o layout para este fragmento
View root = inflater.inflate(R.layout.fragment_definicoes, container, false);

View File

@@ -10,11 +10,14 @@ import android.view.ViewGroup;
import com.example.lifegrid.R;
/**
* DocumentosFragment é a área orientada ao scan, compilação de faturas e arquivo seguro.
* Foca-se unicamente nas componentes não contabéis mas sim em anotações em anexo faturado.
*/
public class DocumentosFragment extends Fragment {
public DocumentosFragment() {
// Required empty public constructor
// Construtor público vazio obrigatório
}
@Override
@@ -25,7 +28,7 @@ public class DocumentosFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
// Inflaciona o layout para este fragmento
View root = inflater.inflate(R.layout.fragment_documentos, container, false);

View File

@@ -10,10 +10,14 @@ import android.view.ViewGroup;
import com.example.lifegrid.R;
/**
* GraficosFragment tem como único propósito compilar a informação da aplicação sob uma
* componente puramente matemática exibida de forma ilustrativa. Tarta da visão de Dashboard de estatística.
*/
public class GraficosFragment extends Fragment {
public GraficosFragment() {
// Required empty public constructor
// Construtor público vazio obrigatório
}
@Override
@@ -24,7 +28,7 @@ public class GraficosFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
// Inflaciona o layout para este fragmento
View root = inflater.inflate(R.layout.fragment_graficos, container, false);

View File

@@ -10,10 +10,14 @@ import android.view.ViewGroup;
import com.example.lifegrid.R;
/**
* MetasFragment projeta objetivos futuros onde o utilizador fixa montantes e datas a alcançar
* sendo apresentada percentagem de sucesso à medida que deposita fundos dedicados.
*/
public class MetasFragment extends Fragment {
public MetasFragment() {
// Required empty public constructor
// Construtor público vazio obrigatório
}
@Override
@@ -24,7 +28,7 @@ public class MetasFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
// Inflaciona o layout para este fragmento
View root = inflater.inflate(R.layout.fragment_metas, container, false);

View File

@@ -8,15 +8,40 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.app.AlertDialog;
import android.app.DatePickerDialog;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import java.util.Calendar;
import java.util.Locale;
import com.example.lifegrid.R;
import com.example.lifegrid.models.Transacao;
import com.google.firebase.Firebase;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
/**
* TransacoesFragment controla a aba pertencente às listagens e histórico de carteira e gestão de dados base.
* É responsável por permitir o lançamento do pop-up para a construção de novas entradas financeiras (receitas/despesas).
*/
public class TransacoesFragment extends Fragment {
public TransacoesFragment() {
// Required empty public constructor
// Construtor público vazio obrigatório
}
@@ -29,10 +54,93 @@ public class TransacoesFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
// Inflaciona o layout para este fragmento
View root = inflater.inflate(R.layout.fragment_transacoes, container, false);
Button novaTransacaoButton = root.findViewById(R.id.novaTransacaoButton);
novaTransacaoButton.setOnClickListener(v -> showNovaTransacaoDialog());
return root;
}
/**
* Cria e monta manualmente uma janela Modal (Pop-up) a fim do utilizador preencher
* os detalhes referentes a uma recém aquisição de receita ou encargo para alimentar a base de dados.
*/
private void showNovaTransacaoDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
View dialogView = getLayoutInflater().inflate(R.layout.dialog_nova_transacao, null);
builder.setView(dialogView);
AlertDialog dialog = builder.create();
if (dialog.getWindow() != null) {
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
}
ImageView btnFechar = dialogView.findViewById(R.id.btnFechar);
btnFechar.setOnClickListener(v -> dialog.dismiss());
EditText etData = dialogView.findViewById(R.id.etData);
etData.setOnClickListener(v -> {
Calendar calendar = Calendar.getInstance();
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int day = calendar.get(Calendar.DAY_OF_MONTH);
DatePickerDialog datePickerDialog = new DatePickerDialog(requireContext(),
(view, selectedYear, selectedMonth, selectedDay) -> {
String formattedDate = String.format(Locale.getDefault(), "%02d/%02d/%04d", selectedDay, selectedMonth + 1, selectedYear);
etData.setText(formattedDate);
}, year, month, day);
datePickerDialog.show();
});
Button btnAdicionarTransacao = dialogView.findViewById(R.id.btnAdicionarTransacao);
EditText etValor = dialogView.findViewById(R.id.etValor);
EditText etDescricao = dialogView.findViewById(R.id.etDescricao);
Spinner spinnerCategoria = dialogView.findViewById(R.id.spinnerCategoria);
Spinner spinnerTipo = dialogView.findViewById(R.id.spinnerTipo);
spinnerTipo.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
int arrayResId = position == 0 ? R.array.categoria_receita : R.array.categoria_despesa;
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(requireContext(),
arrayResId, android.R.layout.simple_spinner_item);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerCategoria.setAdapter(adapter);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
btnAdicionarTransacao.setOnClickListener(v -> {
String valor = etValor.getText().toString().trim();
String descricao = etDescricao.getText().toString().trim();
String data = etData.getText().toString().trim();
String tipo = spinnerTipo.getSelectedItem().toString();
String categoria = spinnerCategoria.getSelectedItem().toString();
Transacao transacao = new Transacao(tipo, valor, categoria, descricao, data);
if (valor.isEmpty() || descricao.isEmpty() || data.isEmpty() || spinnerCategoria.getSelectedItemPosition() == 0) {
Toast.makeText(requireContext(), "Por favor, preencha todos os campos.", Toast.LENGTH_SHORT).show();
} else {
DatabaseReference databaseReference = FirebaseDatabase.getInstance().getReference();
String userId = FirebaseAuth.getInstance().getCurrentUser().getUid();
String key = databaseReference.child("users").child(userId).child("transacoes").push().getKey();
databaseReference.child("users").child(userId).child("transacoes").child(key).setValue(transacao);
dialog.dismiss();
Toast.makeText(requireContext(), "Transação adicionada com sucesso!", Toast.LENGTH_SHORT).show();
}
});
dialog.show();
}
}

View File

@@ -0,0 +1,61 @@
package com.example.lifegrid.models;
public class Transacao {
private String tipo;
private String valor;
private String categoria;
private String descricao;
private String data;
public Transacao() {
}
public Transacao(String tipo, String valor, String categoria, String descricao, String data) {
this.tipo = tipo;
this.valor = valor;
this.categoria = categoria;
this.descricao = descricao;
this.data = data;
}
public String getTipo() {
return tipo;
}
public void setTipo(String tipo) {
this.tipo = tipo;
}
public String getValor() {
return valor;
}
public void setValor(String valor) {
this.valor = valor;
}
public String getCategoria() {
return categoria;
}
public void setCategoria(String categoria) {
this.categoria = categoria;
}
public String getDescricao() {
return descricao;
}
public void setDescricao(String descricao) {
this.descricao = descricao;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/branco"/>
<corners android:radius="20dp"/>
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#F2F2F2" />
<corners android:radius="12dp" />
</shape>

View File

@@ -0,0 +1,154 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/dialog_bg"
android:orientation="vertical"
android:padding="24dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp">
<TextView
android:id="@+id/dialogTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Adicionar Transação"
android:textColor="@color/preto"
android:textSize="20sp"
android:textStyle="bold" />
<ImageView
android:id="@+id/btnFechar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:src="@android:drawable/ic_menu_close_clear_cancel"
app:tint="@color/preto" />
</RelativeLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="Tipo"
android:textColor="@color/preto"
android:textStyle="bold" />
<Spinner
android:id="@+id/spinnerTipo"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginBottom="16dp"
android:background="@drawable/rounded_input_bg"
android:entries="@array/tipo_transacao"
android:popupBackground="@drawable/dialog_bg"
android:paddingHorizontal="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="Valor (€)"
android:textColor="@color/preto"
android:textStyle="bold" />
<EditText
android:id="@+id/etValor"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginBottom="16dp"
android:background="@drawable/rounded_input_bg"
android:hint="0.00"
android:inputType="numberDecimal"
android:paddingHorizontal="16dp"
android:textColor="@color/preto" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="Categoria"
android:textColor="@color/preto"
android:textStyle="bold" />
<Spinner
android:id="@+id/spinnerCategoria"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginBottom="16dp"
android:background="@drawable/rounded_input_bg"
android:popupBackground="@drawable/dialog_bg"
android:paddingHorizontal="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="Descrição"
android:textColor="@color/preto"
android:textStyle="bold" />
<EditText
android:id="@+id/etDescricao"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginBottom="16dp"
android:background="@drawable/rounded_input_bg"
android:hint="Ex: Compras no supermercado"
android:inputType="text"
android:paddingHorizontal="16dp"
android:textColor="@color/preto" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="Data"
android:textColor="@color/preto"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginBottom="16dp"
android:background="@drawable/rounded_input_bg"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="16dp">
<EditText
android:id="@+id/etData"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@null"
android:hint="21/04/2026"
android:focusable="false"
android:clickable="true"
android:inputType="none"
android:textColor="@color/preto" />
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_today"
app:tint="@color/cinzaescuro" />
</LinearLayout>
<Button
android:id="@+id/btnAdicionarTransacao"
android:layout_width="match_parent"
android:layout_height="60dp"
android:backgroundTint="@color/preto"
android:text="Adicionar Transação"
android:textColor="@color/branco"
android:textSize="16sp"
app:cornerRadius="12dp" />
</LinearLayout>

View File

@@ -75,6 +75,7 @@
app:layout_constraintTop_toBottomOf="@+id/textView12" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvTransacoes"
android:layout_width="match_parent"
android:layout_height="211dp"
android:layout_marginStart="16dp"

View File

@@ -4,4 +4,29 @@
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
<!-- Substitua pelo Web Client ID do Firebase Console -> Authentication -> Sign-in method -> Google -->
<string-array name="tipo_transacao">
<item>Receita</item>
<item>Despesa</item>
</string-array>
<string-array name="categoria_despesa">
<item>Selecione uma categoria</item>
<item>Renda</item>
<item>Alimentação</item>
<item>Lazer</item>
<item>Transporte</item>
<item>Saúde</item>
<item>Educação</item>
<item>Utilities</item>
<item>Outros</item>
</string-array>
<string-array name="categoria_receita">
<item>Selecione uma categoria</item>
<item>Salário</item>
<item>Freelance</item>
<item>Investimentos</item>
<item>Outros</item>
</string-array>
</resources>