Compare commits

...

16 Commits

Author SHA1 Message Date
a1a01e27df depois do ollama 2026-06-18 11:49:48 +01:00
31034a3465 depois do ollama 2026-06-18 11:26:04 +01:00
75ba56cf74 depois do ollama 2026-06-18 11:20:10 +01:00
55560a1bfb antes do ollama 2026-06-18 11:15:49 +01:00
04ce7ece4f a acabar 2026-06-18 10:19:35 +01:00
4356a7432e a acabar 2026-06-15 17:09:47 +01:00
196487a2e2 a acabar 2026-06-15 15:32:58 +01:00
53f77a852e a acabar 2026-06-15 15:20:40 +01:00
21c1e472ba Revert "a acabar"
This reverts commit 533f164b0f.
2026-06-15 15:19:12 +01:00
533f164b0f a acabar 2026-06-15 14:56:55 +01:00
9204ddd4ce a acabar 2026-06-11 11:49:13 +01:00
65dce225de a acabar 2026-06-08 17:06:54 +01:00
148c104b04 ativos 2026-05-12 17:14:34 +01:00
53b791a5ce graficos 2026-05-11 17:18:00 +01:00
413019cfac graficos 2026-05-11 15:00:55 +01:00
e7ead2a066 last 2026-05-08 19:22:31 +01:00
41 changed files with 1148 additions and 453 deletions

View File

@@ -4,10 +4,43 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-04-23T16:33:30.001147200Z">
<DropdownSelection timestamp="2026-06-18T10:01:02.387533Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\Rodrigo\.android\avd\Medium_Phone.avd" />
<DeviceId pluginId="PhysicalDevice" identifier="serial=RZCX40Q6DDY" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="main">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-06-03T14:39:06.554406Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/230415/.android/avd/Pixel_9a.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="unitTest">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-06-03T14:39:06.554406Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/230415/.android/avd/Pixel_9a.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="androidTest">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-06-03T14:39:06.554406Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/230415/.android/avd/Pixel_9a.avd" />
</handle>
</Target>
</DropdownSelection>

View File

@@ -68,4 +68,5 @@ dependencies {
androidTestImplementation(libs.ui.test.junit4)
debugImplementation(libs.ui.tooling)
debugImplementation(libs.ui.test.manifest)
implementation("com.github.PhilJay:MPAndroidChart:v3.1.0")
}

View File

@@ -19,6 +19,9 @@
<activity
android:name=".DefinicoesActivity"
android:exported="false" />
<activity
android:name=".TermosPoliticasActivity"
android:exported="false" />
<activity
android:name=".TelaInicialActivity"
android:exported="false" />

View File

@@ -7,8 +7,10 @@ import android.util.Patterns;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.Toast;
import com.example.lifegrid.utils.CustomToast;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
@@ -16,24 +18,9 @@ import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.credentials.Credential;
import androidx.credentials.CustomCredential;
import androidx.credentials.GetCredentialRequest;
import androidx.credentials.GetCredentialResponse;
import androidx.credentials.exceptions.GetCredentialException;
import androidx.credentials.exceptions.NoCredentialException;
import androidx.credentials.CredentialManager;
import com.google.android.libraries.identity.googleid.GetGoogleIdOption;
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential;
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException;
import com.google.firebase.FirebaseApp;
import com.google.firebase.auth.AuthCredential;
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.
@@ -47,10 +34,8 @@ public class CriarContaActivity extends AppCompatActivity {
private EditText passwordEditText3;
private EditText passwordEditText2;
private Button loginButton2;
private Button googleButton2;
private ProgressBar loadingProgressBar;
private FirebaseAuth firebaseAuth;
private CredentialManager credentialManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -68,15 +53,16 @@ public class CriarContaActivity extends AppCompatActivity {
passwordEditText3 = findViewById(R.id.passwordEditText3);
passwordEditText2 = findViewById(R.id.passwordEditText2);
loginButton2 = findViewById(R.id.loginButton2);
googleButton2 = findViewById(R.id.googleButton2);
loadingProgressBar = findViewById(R.id.loadingProgressBar);
FirebaseApp.initializeApp(this);
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());
ImageView btnBack = findViewById(R.id.btnBack);
btnBack.setOnClickListener(v -> finish());
}
/**
@@ -102,9 +88,7 @@ public class CriarContaActivity extends AppCompatActivity {
FirebaseUser user = firebaseAuth.getCurrentUser();
if (user != null) {
// Conta criada com sucesso - redirecionar para TelaInicialActivity
Toast.makeText(this,
"Conta criada com sucesso! Bem-vindo, " + nome,
Toast.LENGTH_SHORT).show();
CustomToast.success(this, "Conta criada com sucesso! Bem-vindo, " + nome);
// Redirecionar para TelaInicialActivity apenas após sucesso
Intent intent = new Intent(CriarContaActivity.this, TelaInicialActivity.class);
@@ -126,7 +110,7 @@ public class CriarContaActivity extends AppCompatActivity {
errorMessage = "Palavra-passe muito fraca. Use uma palavra-passe mais forte.";
}
}
Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show();
CustomToast.error(this, errorMessage);
}
});
}
@@ -191,35 +175,9 @@ public class CriarContaActivity extends AppCompatActivity {
return true;
}
private void firebaseAuthWithGoogle(String idToken) {
AuthCredential credential = GoogleAuthProvider.getCredential(idToken, null);
firebaseAuth.signInWithCredential(credential)
.addOnCompleteListener(this, task -> {
toggleLoading(false);
if (task.isSuccessful()) {
FirebaseUser user = firebaseAuth.getCurrentUser();
String welcome = user != null && !TextUtils.isEmpty(user.getDisplayName())
? "Conta criada com sucesso! Bem-vindo, " + user.getDisplayName()
: "Conta criada com Google com sucesso!";
Toast.makeText(this, welcome, Toast.LENGTH_SHORT).show();
// Redirecionar para TelaInicialActivity após criação bem-sucedida
Intent intent = new Intent(CriarContaActivity.this, TelaInicialActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
finish();
} else {
Toast.makeText(this,
task.getException() != null ? task.getException().getMessage() : "Falha ao criar conta com Google",
Toast.LENGTH_LONG).show();
}
});
}
private void toggleLoading(boolean show) {
loadingProgressBar.setVisibility(show ? View.VISIBLE : View.GONE);
loginButton2.setEnabled(!show);
googleButton2.setEnabled(!show);
nomeEditText.setEnabled(!show);
emailEditText2.setEnabled(!show);
passwordEditText3.setEnabled(!show);

View File

@@ -9,6 +9,7 @@ import android.widget.Spinner;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;
import com.example.lifegrid.utils.CustomToast;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.FileProvider;
@@ -36,7 +37,7 @@ public class DefinicoesActivity extends AppCompatActivity {
private Button btnSaveSettings;
private EditText etUsername;
private Switch switchNotifications;
private Spinner spinnerCurrency;
private ImageView ivProfilePicture;
private TextView tvChangePhoto;
@@ -62,7 +63,7 @@ public class DefinicoesActivity extends AppCompatActivity {
btnSaveSettings = findViewById(R.id.btnSaveSettings);
etUsername = findViewById(R.id.etUsername);
switchNotifications = findViewById(R.id.switchNotifications);
spinnerCurrency = findViewById(R.id.spinnerCurrency);
ivProfilePicture = findViewById(R.id.ivProfilePicture);
tvChangePhoto = findViewById(R.id.tvChangePhoto);
@@ -90,6 +91,9 @@ public class DefinicoesActivity extends AppCompatActivity {
String savedName = prefs.getString("username", "");
String savedPhotoUri = prefs.getString("profile_photo_uri", "");
boolean savedNotifications = prefs.getBoolean("notifications_enabled", true);
switchNotifications.setChecked(savedNotifications);
if (!savedPhotoUri.isEmpty()) {
selectedImageUri = Uri.parse(savedPhotoUri);
try {
@@ -108,10 +112,22 @@ public class DefinicoesActivity extends AppCompatActivity {
}
}
TextView tvDocuments = findViewById(R.id.tvDocuments);
if (tvDocuments != null) {
tvDocuments.setOnClickListener(v -> {
Intent intent = new Intent(DefinicoesActivity.this, DocumentosActivity.class);
TextView tvTerms = findViewById(R.id.tvTerms);
if (tvTerms != null) {
tvTerms.setOnClickListener(v -> {
Intent intent = new Intent(DefinicoesActivity.this, TermosPoliticasActivity.class);
intent.putExtra("tipo_documento", "terms");
startActivity(intent);
});
}
TextView tvPrivacy = findViewById(R.id.tvPrivacy);
if (tvPrivacy != null) {
tvPrivacy.setOnClickListener(v -> {
Intent intent = new Intent(DefinicoesActivity.this, TermosPoliticasActivity.class);
intent.putExtra("tipo_documento", "privacy");
startActivity(intent);
});
}
@@ -131,6 +147,7 @@ public class DefinicoesActivity extends AppCompatActivity {
btnSaveSettings.setOnClickListener(v -> {
SharedPreferences.Editor editor = getSharedPreferences("LifeGridPrefs", Context.MODE_PRIVATE).edit();
editor.putString("username", etUsername.getText().toString().trim());
editor.putBoolean("notifications_enabled", switchNotifications.isChecked());
if (selectedImageUri != null) {
editor.putString("profile_photo_uri", selectedImageUri.toString());
try {
@@ -141,7 +158,7 @@ public class DefinicoesActivity extends AppCompatActivity {
}
editor.apply();
Toast.makeText(this, "Definições guardadas com sucesso!", Toast.LENGTH_SHORT).show();
CustomToast.success(this, "Definições guardadas com sucesso!");
finish();
});
}

View File

@@ -8,6 +8,7 @@ import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.example.lifegrid.utils.CustomToast;
import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
@@ -98,7 +99,7 @@ public class DocumentosActivity extends AppCompatActivity {
@Override
public void onCancelled(@NonNull DatabaseError error) {
Toast.makeText(DocumentosActivity.this, "Erro ao carregar documentos.", Toast.LENGTH_SHORT).show();
CustomToast.error(DocumentosActivity.this, "Erro ao carregar documentos.");
}
});
}

View File

@@ -4,26 +4,26 @@ import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.util.Base64;
import android.util.Log;
import com.google.ai.client.generativeai.GenerativeModel;
import com.google.ai.client.generativeai.java.GenerativeModelFutures;
import com.google.ai.client.generativeai.type.Content;
import com.google.ai.client.generativeai.type.GenerateContentResponse;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.json.JSONException;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class InvoiceScannerHelper {
private static final String API_KEY = "AIzaSyCoUZSXfEk43LfPtkCCjsnQ_ZMWX7NG1xQ"; // Substitua pela sua chave API
private static final String API_URL = "https://apichat.epvc.pt/api/chat";
private static final String MODEL_NAME = "qwen3.6";
private static final String PROMPT = "Analisa esta fatura. Devolve APENAS um objeto JSON com os campos: 'valor' (Double, apenas números e ponto decimal), 'descricao' (String curta, nome do estabelecimento ou produto), 'categoria' (String, ex: Alimentação, Transporte, Lazer, Saúde, Casa, Educação, Outros), e 'data' (String formato DD/MM/AAAA). Se não conseguires ler algum, coloca valor padrão ou string vazia.";
public interface ScanCallback {
@@ -32,75 +32,119 @@ public class InvoiceScannerHelper {
}
public static void scanInvoice(Context context, Uri imageUri, ScanCallback callback) {
if (API_KEY == null || API_KEY.isEmpty() || API_KEY.contains("CHAVE_API_KEY")) {
callback.onError("Chave API do Gemini não configurada.");
return;
}
Executor executor = Executors.newSingleThreadExecutor();
try (InputStream imageStream = context.getContentResolver().openInputStream(imageUri)) {
Bitmap bitmap = BitmapFactory.decodeStream(imageStream);
executor.execute(() -> {
try (InputStream imageStream = context.getContentResolver().openInputStream(imageUri)) {
Bitmap originalBitmap = BitmapFactory.decodeStream(imageStream);
if (bitmap == null) {
callback.onError("Não foi possível carregar a imagem da fatura.");
return;
}
GenerativeModel gm = new GenerativeModel(
"gemini-2.5-flash",
API_KEY
);
GenerativeModelFutures model = GenerativeModelFutures.from(gm);
Content content = new Content.Builder()
.addText(PROMPT)
.addImage(bitmap)
.build();
ListenableFuture<GenerateContentResponse> response = model.generateContent(content);
Futures.addCallback(response, new FutureCallback<GenerateContentResponse>() {
@Override
public void onSuccess(GenerateContentResponse result) {
try {
String textResponse = result.getText();
if (textResponse != null) {
textResponse = textResponse.replace("```json", "").replace("```", "").trim();
JSONObject json = new JSONObject(textResponse);
double valor = json.optDouble("valor", 0.0);
String descricao = json.optString("descricao", "");
String categoria = json.optString("categoria", "Outros");
String data = json.optString("data", "");
callback.onSuccess(valor, descricao, categoria, data);
} else {
callback.onError("Resposta vazia da IA.");
}
} catch (JSONException e) {
Log.e("InvoiceScanner", "Erro ao fazer parse do JSON: " + result.getText(), e);
callback.onError("Erro ao ler dados da fatura.");
} finally {
if (executor instanceof java.util.concurrent.ExecutorService) {
((java.util.concurrent.ExecutorService) executor).shutdown();
}
}
if (originalBitmap == null) {
callback.onError("Não foi possível carregar a imagem da fatura.");
return;
}
@Override
public void onFailure(Throwable t) {
Log.e("InvoiceScanner", "Falha na API do Gemini", t);
callback.onError("Falha ao comunicar com a IA: " + t.getMessage());
if (executor instanceof java.util.concurrent.ExecutorService) {
((java.util.concurrent.ExecutorService) executor).shutdown();
}
}
}, executor);
// Redimensionar a imagem para otimizar o envio
Bitmap bitmap = scaleBitmap(originalBitmap, 1024);
String base64Image = bitmapToBase64(bitmap);
} catch (Exception e) {
Log.e("InvoiceScanner", "Erro geral", e);
callback.onError("Erro ao processar imagem: " + e.getMessage());
if (executor instanceof java.util.concurrent.ExecutorService) {
((java.util.concurrent.ExecutorService) executor).shutdown();
// Criar o payload JSON para o Ollama
JSONObject payload = new JSONObject();
payload.put("model", MODEL_NAME);
payload.put("stream", false);
payload.put("format", "json");
JSONArray messages = new JSONArray();
JSONObject message = new JSONObject();
message.put("role", "user");
message.put("content", PROMPT);
JSONArray images = new JSONArray();
images.put(base64Image);
message.put("images", images);
messages.put(message);
payload.put("messages", messages);
// Enviar a requisição HTTP
URL url = new URL(API_URL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
conn.setRequestProperty("Accept", "application/json");
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setConnectTimeout(60000); // 60 segundos de timeout
conn.setReadTimeout(60000);
try (OutputStream os = conn.getOutputStream()) {
byte[] input = payload.toString().getBytes("utf-8");
os.write(input, 0, input.length);
}
int responseCode = conn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
StringBuilder response = new StringBuilder();
String responseLine;
while ((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
br.close();
JSONObject responseJson = new JSONObject(response.toString());
JSONObject messageObj = responseJson.getJSONObject("message");
String contentStr = messageObj.getString("content");
// Tratar a extração robusta do JSON
int start = contentStr.indexOf("{");
int end = contentStr.lastIndexOf("}");
if (start != -1 && end != -1 && end > start) {
String jsonContent = contentStr.substring(start, end + 1);
JSONObject invoiceJson = new JSONObject(jsonContent);
double valor = invoiceJson.optDouble("valor", 0.0);
String descricao = invoiceJson.optString("descricao", "");
String categoria = invoiceJson.optString("categoria", "Outros");
String data = invoiceJson.optString("data", "");
callback.onSuccess(valor, descricao, categoria, data);
} else {
callback.onError("Não foi possível extrair os dados formatados da fatura.");
}
} else {
callback.onError("Erro ao comunicar com a IA: Servidor respondeu com código " + responseCode);
}
} catch (Exception e) {
Log.e("InvoiceScanner", "Erro geral", e);
callback.onError("Erro ao processar fatura: " + e.getMessage());
} finally {
if (executor instanceof java.util.concurrent.ExecutorService) {
((java.util.concurrent.ExecutorService) executor).shutdown();
}
}
});
}
private static Bitmap scaleBitmap(Bitmap bitmap, int maxDimension) {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
if (width <= maxDimension && height <= maxDimension) {
return bitmap;
}
float ratio = (float) width / (float) height;
int newWidth, newHeight;
if (width > height) {
newWidth = maxDimension;
newHeight = Math.round(maxDimension / ratio);
} else {
newHeight = maxDimension;
newWidth = Math.round(maxDimension * ratio);
}
return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true);
}
private static String bitmapToBase64(Bitmap bitmap) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream);
byte[] byteArray = outputStream.toByteArray();
return Base64.encodeToString(byteArray, Base64.NO_WRAP);
}
}

View File

@@ -14,6 +14,7 @@ import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.example.lifegrid.utils.CustomToast;
import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
@@ -22,28 +23,9 @@ import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.credentials.ClearCredentialStateRequest;
import androidx.credentials.CredentialManager;
import androidx.credentials.Credential;
import androidx.credentials.CredentialManagerCallback;
import androidx.credentials.CustomCredential;
import androidx.credentials.GetCredentialRequest;
import androidx.credentials.GetCredentialResponse;
import androidx.credentials.exceptions.ClearCredentialException;
import androidx.credentials.exceptions.GetCredentialException;
import androidx.credentials.exceptions.NoCredentialException;
import com.google.android.libraries.identity.googleid.GetGoogleIdOption;
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential;
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException;
import com.google.firebase.FirebaseApp;
import com.google.firebase.auth.AuthCredential;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
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.
@@ -61,16 +43,10 @@ public class LoginActivity extends AppCompatActivity {
private EditText passwordEditText;
private TextView passesquecerTextView;
private Button loginButton;
private Button googleButton;
private TextView ouTextView;
private ProgressBar loadingProgressBar;
private FirebaseAuth firebaseAuth;
private CredentialManager credentialManager;
private FirebaseAuth mAuth;
private static final String TAG = "LoginActivity - Google Sign In";
private static final String GOOGLE_ID_TOKEN_CREDENTIAL = "1019731295596-i3q6aprqj6s55g6s97tpopbk4foutold.apps.googleusercontent.com";
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -91,17 +67,13 @@ public class LoginActivity extends AppCompatActivity {
passwordEditText = findViewById(R.id.passwordEditText);
passesquecerTextView = findViewById(R.id.passesquecerTextView);
loginButton = findViewById(R.id.loginButton);
googleButton = findViewById(R.id.googleButton);
ouTextView = findViewById(R.id.ouTextView);
loadingProgressBar = findViewById(R.id.loadingProgressBar);
FirebaseApp.initializeApp(this);
firebaseAuth = FirebaseAuth.getInstance();
mAuth = FirebaseAuth.getInstance();
credentialManager = CredentialManager.create(getBaseContext());
loginButton.setOnClickListener(v -> validarLogin());
googleButton.setOnClickListener(v -> launchCredentialManager());
criarContaTextView.setOnClickListener(new View.OnClickListener() {
@Override
@@ -124,7 +96,7 @@ public class LoginActivity extends AppCompatActivity {
if (TextUtils.isEmpty(email)) {
emailEditText.setError("Por favor, digite o seu e-mail primeiro.");
emailEditText.requestFocus();
Toast.makeText(LoginActivity.this, "Digite o seu e-mail para recuperar a palavra-passe.", Toast.LENGTH_SHORT).show();
CustomToast.info(LoginActivity.this, "Digite o seu e-mail para recuperar a palavra-passe.");
return;
}
@@ -174,7 +146,7 @@ public class LoginActivity extends AppCompatActivity {
String welcome = user != null && !TextUtils.isEmpty(user.getEmail())
? "Bem-vindo, " + user.getEmail()
: "Login realizado com sucesso!";
Toast.makeText(this, welcome, Toast.LENGTH_SHORT).show();
CustomToast.success(this, welcome);
// Redirecionar para TelaInicialActivity após login bem-sucedido
Intent intent = new Intent(LoginActivity.this, TelaInicialActivity.class);
@@ -182,9 +154,8 @@ public class LoginActivity extends AppCompatActivity {
startActivity(intent);
finish(); // Fechar LoginActivity para não poder voltar com back button
} else {
Toast.makeText(this,
task.getException() != null ? task.getException().getMessage() : "Falha no login",
Toast.LENGTH_LONG).show();
CustomToast.error(this,
task.getException() != null ? task.getException().getMessage() : "Falha no login");
}
});
}
@@ -202,13 +173,11 @@ public class LoginActivity extends AppCompatActivity {
.addOnCompleteListener(this, task -> {
toggleLoading(false);
if (task.isSuccessful()) {
Toast.makeText(this,
"Email de recuperação enviado para " + email,
Toast.LENGTH_LONG).show();
CustomToast.success(this,
"Email de recuperação enviado para " + email);
} else {
Toast.makeText(this,
task.getException() != null ? task.getException().getMessage() : "Erro ao enviar email",
Toast.LENGTH_LONG).show();
CustomToast.error(this,
task.getException() != null ? task.getException().getMessage() : "Erro ao enviar email");
}
});
}
@@ -242,113 +211,13 @@ public class LoginActivity extends AppCompatActivity {
}
private void firebaseAuthWithGoogle(String idToken) {
AuthCredential credential = GoogleAuthProvider.getCredential(idToken, null);
firebaseAuth.signInWithCredential(credential)
.addOnCompleteListener(this, task -> {
toggleLoading(false);
if (task.isSuccessful()) {
FirebaseUser user = firebaseAuth.getCurrentUser();
String welcome = user != null && !TextUtils.isEmpty(user.getDisplayName())
? "Bem-vindo, " + user.getDisplayName()
: "Login com Google realizado com sucesso!";
Toast.makeText(this, welcome, Toast.LENGTH_SHORT).show();
// Redirecionar para TelaInicialActivity após login bem-sucedido
Intent intent = new Intent(LoginActivity.this, TelaInicialActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
finish();
} else {
Toast.makeText(this,
task.getException() != null ? task.getException().getMessage() : "Falha no login com Google",
Toast.LENGTH_LONG).show();
}
});
}
private void toggleLoading(boolean show) {
loadingProgressBar.setVisibility(show ? View.VISIBLE : View.GONE);
loginButton.setEnabled(!show);
googleButton.setEnabled(!show);
criarContaTextView.setEnabled(!show);
passesquecerTextView.setEnabled(!show);
}
private void launchCredentialManager() {
// [START create_credential_manager_request]
// Instancia um pedido de início de sessão do Google
GetGoogleIdOption googleIdOption = new GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(false)
.setServerClientId(getString(R.string.default_web_client_id))
.build();
// Cria o pedido do Gestor de Credenciais
GetCredentialRequest request = new GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build();
// [END create_credential_manager_request]
// Lança a interface do Gestor de Credenciais
credentialManager.getCredentialAsync(
LoginActivity.this,
request,
new CancellationSignal(),
androidx.core.content.ContextCompat.getMainExecutor(LoginActivity.this),
new CredentialManagerCallback<GetCredentialResponse, GetCredentialException>() {
@Override
public void onResult(GetCredentialResponse result) {
// Extrai a credencial do resultado devolvido pelo Gestor de Credenciais
handleSignIn(result.getCredential());
}
@Override
public void onError(GetCredentialException e) {
Log.e(TAG, "Couldn't retrieve user's credentials: " + e.getLocalizedMessage());
Toast.makeText(LoginActivity.this, "Falha ao abrir Google Sign In.", Toast.LENGTH_SHORT).show();
}
}
);
}
private void handleSignIn(Credential credential) {
// Verifica se a credencial é do tipo Google ID
if (credential instanceof CustomCredential customCredential
&& credential.getType().equals(GOOGLE_ID_TOKEN_CREDENTIAL)) {
// Cria o token do Google ID
Bundle credentialData = customCredential.getData();
GoogleIdTokenCredential googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credentialData);
// Inicia a sessão no Firebase usando o token
firebaseAuthWithGoogle(googleIdTokenCredential.getIdToken());
} else {
Log.w(TAG, "Credential is not of type Google ID!");
}
}
private void signOut() {
// Termina a sessão no Firebase
mAuth.signOut();
// 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,
new CancellationSignal(),
Executors.newSingleThreadExecutor(),
new CredentialManagerCallback<>() {
@Override
public void onResult(@NonNull Void result) {
//updateUI(null);
}
@Override
public void onError(@NonNull ClearCredentialException e) {
Log.e(TAG, "Couldn't clear user credentials: " + e.getLocalizedMessage());
}
});
}
@Override
public void onStart() {
super.onStart();

View File

@@ -8,6 +8,8 @@ import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.Toast;
import android.widget.ImageView;
import com.example.lifegrid.utils.CustomToast;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
@@ -58,6 +60,9 @@ public class RecupearPasswordActivity extends AppCompatActivity {
}
loginButton3.setOnClickListener(v -> recuperarPassword());
ImageView btnBack = findViewById(R.id.btnBack);
btnBack.setOnClickListener(v -> finish());
}
/**
@@ -92,9 +97,8 @@ public class RecupearPasswordActivity extends AppCompatActivity {
toggleLoading(false);
if (task.isSuccessful()) {
// Email enviado com sucesso
Toast.makeText(this,
"Email de recuperação enviado para " + email + "\nVerifique sua caixa de entrada.",
Toast.LENGTH_LONG).show();
CustomToast.success(this,
"Email de recuperação enviado para " + email + "\nVerifique sua caixa de entrada.");
// Limpar o campo após sucesso
emailEditText4.setText("");
} else {
@@ -113,7 +117,7 @@ public class RecupearPasswordActivity extends AppCompatActivity {
errorMessage = "Muitas tentativas. Tente novamente mais tarde.";
}
}
Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show();
CustomToast.error(this, errorMessage);
}
});
}

View File

@@ -25,6 +25,7 @@ import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.Toast;
import com.example.lifegrid.utils.CustomToast;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.database.DatabaseReference;
@@ -155,7 +156,7 @@ public class TelaInicialActivity extends AppCompatActivity {
}
private void processInvoiceImage(android.net.Uri imageUri) {
Toast.makeText(this, "A processar fatura com IA...", Toast.LENGTH_LONG).show();
CustomToast.info(this, "A processar fatura com IA...");
InvoiceScannerHelper.scanInvoice(this, imageUri, new InvoiceScannerHelper.ScanCallback() {
@Override
public void onSuccess(double valor, String descricao, String categoria, String data) {
@@ -174,49 +175,26 @@ public class TelaInicialActivity extends AppCompatActivity {
}
transFragment.showNovaTransacaoDialog(valor, descricao, categoria, data);
// Save document to Firebase
uploadDocumentoToFirebase(imageUri, descricao, data);
deleteLocalInvoiceFile(imageUri);
});
}
@Override
public void onError(String error) {
runOnUiThread(() -> Toast.makeText(TelaInicialActivity.this, error, Toast.LENGTH_LONG).show());
runOnUiThread(() -> {
CustomToast.error(TelaInicialActivity.this, error);
deleteLocalInvoiceFile(imageUri);
});
}
});
}
private void uploadDocumentoToFirebase(android.net.Uri imageUri, String descricao, String data) {
FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
if (user == null) {
Toast.makeText(this, "Utilizador não autenticado.", Toast.LENGTH_SHORT).show();
return;
private void deleteLocalInvoiceFile(android.net.Uri uri) {
try {
getContentResolver().delete(uri, null, null);
} catch (Exception e) {
e.printStackTrace();
}
Toast.makeText(this, "A guardar documento na Cloud...", Toast.LENGTH_SHORT).show();
String fileName = "invoice_" + System.currentTimeMillis() + ".jpg";
com.google.firebase.storage.StorageReference storageRef = com.google.firebase.storage.FirebaseStorage.getInstance().getReference()
.child("users/" + user.getUid() + "/documentos/" + fileName);
storageRef.putFile(imageUri).addOnSuccessListener(taskSnapshot -> {
storageRef.getDownloadUrl().addOnSuccessListener(uri -> {
String downloadUrl = uri.toString();
DatabaseReference dbRef = FirebaseDatabase.getInstance().getReference("users")
.child(user.getUid()).child("documentos").push();
com.example.lifegrid.models.Documento doc = new com.example.lifegrid.models.Documento(descricao, data, downloadUrl);
dbRef.setValue(doc).addOnCompleteListener(task -> {
if (task.isSuccessful()) {
Toast.makeText(TelaInicialActivity.this, "Documento guardado com sucesso!", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(TelaInicialActivity.this, "Erro ao guardar dados na Cloud.", Toast.LENGTH_SHORT).show();
}
});
});
}).addOnFailureListener(e -> {
Toast.makeText(this, "Erro ao enviar imagem para a Cloud.", Toast.LENGTH_SHORT).show();
});
}
@Override
@@ -298,6 +276,9 @@ public class TelaInicialActivity extends AppCompatActivity {
((HomeFragment) currentFragment).carregarDados(mesSelecionado, anoSelecionado, mesNome);
} else if (currentFragment instanceof TransacoesFragment) {
((TransacoesFragment) currentFragment).setFiltro(mesSelecionado, anoSelecionado);
} else if (currentFragment instanceof GraficosFragment) {
String mesNome = meses[spinnerMes.getSelectedItemPosition()];
((GraficosFragment) currentFragment).setFiltro(mesSelecionado, anoSelecionado, mesNome);
}
}

View File

@@ -0,0 +1,50 @@
package com.example.lifegrid;
import android.os.Bundle;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.text.HtmlCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
public class TermosPoliticasActivity extends AppCompatActivity {
private ImageView btnBack;
private TextView tvTitle;
private TextView tvContent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_termos_politicas);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
btnBack = findViewById(R.id.btnBack);
tvTitle = findViewById(R.id.tvTitle);
tvContent = findViewById(R.id.tvContent);
btnBack.setOnClickListener(v -> finish());
String tipoDocumento = getIntent().getStringExtra("tipo_documento");
if (tipoDocumento != null) {
if (tipoDocumento.equals("terms")) {
tvTitle.setText(R.string.terms_title);
String htmlText = getString(R.string.terms_content);
tvContent.setText(HtmlCompat.fromHtml(htmlText, HtmlCompat.FROM_HTML_MODE_LEGACY));
} else if (tipoDocumento.equals("privacy")) {
tvTitle.setText(R.string.privacy_title);
String htmlText = getString(R.string.privacy_content);
tvContent.setText(HtmlCompat.fromHtml(htmlText, HtmlCompat.FROM_HTML_MODE_LEGACY));
}
}
}
}

View File

@@ -93,7 +93,6 @@ public class AtivosAdapter extends RecyclerView.Adapter<AtivosAdapter.AtivosView
holder.btnRefresh.setOnClickListener(v -> {
if (listener != null) listener.onRefreshClick(position, key, ativo);
Toast.makeText(context, "Ativo atualizado (Exemplo)", Toast.LENGTH_SHORT).show();
});
}

View File

@@ -10,6 +10,7 @@ import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import com.example.lifegrid.utils.CustomToast;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
@@ -47,7 +48,7 @@ public class DocumentoAdapter extends RecyclerView.Adapter<DocumentoAdapter.Docu
if (url != null && !url.isEmpty()) {
downloadFile(url, holder.tvDescricao.getText().toString());
} else {
Toast.makeText(context, "URL inválido para o documento.", Toast.LENGTH_SHORT).show();
CustomToast.error(context, "URL inválido para o documento.");
}
});
}
@@ -70,11 +71,11 @@ public class DocumentoAdapter extends RecyclerView.Adapter<DocumentoAdapter.Docu
DownloadManager manager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
if (manager != null) {
manager.enqueue(request);
Toast.makeText(context, "Transferência iniciada. Verifique as notificações.", Toast.LENGTH_SHORT).show();
CustomToast.info(context, "Transferência iniciada. Verifique as notificações.");
}
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(context, "Erro ao iniciar transferência.", Toast.LENGTH_SHORT).show();
CustomToast.error(context, "Erro ao iniciar transferência.");
}
}

View File

@@ -8,6 +8,7 @@ import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.example.lifegrid.utils.CustomToast;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
@@ -106,7 +107,7 @@ public class MetaAdapter extends RecyclerView.Adapter<MetaAdapter.MetaViewHolder
double valorAdicional = Double.parseDouble(input);
listener.onAddValueClick(meta, valorAdicional, holder.etAdicionarValor);
} catch (NumberFormatException e) {
Toast.makeText(v.getContext(), "Valor inválido", Toast.LENGTH_SHORT).show();
CustomToast.error(v.getContext(), "Valor inválido");
}
}
}

View File

@@ -5,6 +5,8 @@ import android.app.DatePickerDialog;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import androidx.fragment.app.Fragment;
@@ -17,9 +19,19 @@ import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Spinner;
import android.widget.Toast;
import com.example.lifegrid.utils.CustomToast;
import java.util.Calendar;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import org.json.JSONObject;
import org.json.JSONArray;
import com.example.lifegrid.R;
import com.example.lifegrid.adapters.AtivosAdapter;
@@ -87,7 +99,7 @@ public class AtivosFragment extends Fragment {
@Override
public void onRefreshClick(int position, String key, Ativos ativo) {
// A implementar no futuro
refreshAtivoPrice(ativo, key);
}
});
ativosRecyclerView.setAdapter(ativosAdapter);
@@ -148,7 +160,60 @@ public class AtivosFragment extends Fragment {
@Override
public void onCancelled(@NonNull DatabaseError error) {
Toast.makeText(requireContext(), "Erro ao carregar ativos.", Toast.LENGTH_SHORT).show();
CustomToast.error(requireContext(), "Erro ao carregar ativos.");
}
});
}
private void refreshAtivoPrice(Ativos ativo, String key) {
if (ativo.getTicker() == null || ativo.getTicker().isEmpty()) {
CustomToast.info(requireContext(), "Este ativo não tem um Ticker definido.");
return;
}
CustomToast.info(requireContext(), "A atualizar " + ativo.getTicker() + "...");
ExecutorService executor = Executors.newSingleThreadExecutor();
Handler handler = new Handler(Looper.getMainLooper());
executor.execute(() -> {
try {
String ticker = ativo.getTicker().toUpperCase(Locale.ROOT);
URL url = new URL("https://query1.finance.yahoo.com/v8/finance/chart/" + ticker);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("User-Agent", "Mozilla/5.0");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
InputStream in = conn.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringBuilder result = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
result.append(line);
}
reader.close();
JSONObject jsonResponse = new JSONObject(result.toString());
JSONObject chart = jsonResponse.getJSONObject("chart");
JSONArray resultArr = chart.getJSONArray("result");
JSONObject resultObj = resultArr.getJSONObject(0);
JSONObject meta = resultObj.getJSONObject("meta");
double regularMarketPrice = meta.getDouble("regularMarketPrice");
handler.post(() -> {
String userId = FirebaseAuth.getInstance().getCurrentUser().getUid();
databaseReference.child("users").child(userId).child("ativos").child(key).child("precoAtual").setValue(regularMarketPrice);
CustomToast.success(requireContext(), "Preço atualizado: " + regularMarketPrice + "");
});
} else {
handler.post(() -> CustomToast.error(requireContext(), "Erro ao contactar a API."));
}
} catch (Exception e) {
e.printStackTrace();
handler.post(() -> CustomToast.error(requireContext(), "Ativo não encontrado ou erro de rede."));
}
});
}
@@ -183,23 +248,48 @@ public class AtivosFragment extends Fragment {
Button btnAdicionarAtivoDialog = dialogView.findViewById(R.id.btnAdicionarAtivoDialog);
EditText etNomeAtivo = dialogView.findViewById(R.id.etNomeAtivo);
EditText etTickerAtivo = dialogView.findViewById(R.id.etTickerAtivo);
TextView tvTickerLabel = dialogView.findViewById(R.id.tvTickerLabel);
Spinner spinnerTipoAtivo = dialogView.findViewById(R.id.spinnerTipoAtivo);
EditText etQuantidade = dialogView.findViewById(R.id.etQuantidade);
EditText etPrecoCompra = dialogView.findViewById(R.id.etPrecoCompra);
spinnerTipoAtivo.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(android.widget.AdapterView<?> parent, View view, int position, long id) {
String selectedType = parent.getItemAtPosition(position).toString();
if ("Criptomoedas".equals(selectedType)) {
tvTickerLabel.setVisibility(View.VISIBLE);
etTickerAtivo.setVisibility(View.VISIBLE);
} else {
tvTickerLabel.setVisibility(View.GONE);
etTickerAtivo.setVisibility(View.GONE);
etTickerAtivo.setText("");
}
}
@Override
public void onNothingSelected(android.widget.AdapterView<?> parent) {
}
});
btnAdicionarAtivoDialog.setOnClickListener(v -> {
String nome = etNomeAtivo.getText().toString().trim();
String ticker = etTickerAtivo.getText().toString().trim();
String tipo = spinnerTipoAtivo.getSelectedItem().toString();
String quantidade = etQuantidade.getText().toString().trim();
String precoCompra = etPrecoCompra.getText().toString().trim();
String dataCompra = etDataCompra.getText().toString().trim();
Ativos ativos = new Ativos(nome, quantidade, Double.parseDouble(precoCompra), Double.parseDouble(precoCompra), dataCompra, tipo);
boolean isCriptomoeda = "Criptomoedas".equals(tipo);
if (nome.isEmpty() || quantidade.isEmpty() || precoCompra.isEmpty() || dataCompra.isEmpty() || spinnerTipoAtivo.getSelectedItemPosition() == 0) {
Toast.makeText(requireContext(), "Por favor, preencha os campos obrigatórios.", Toast.LENGTH_SHORT).show();
CustomToast.info(requireContext(), "Por favor, preencha os campos obrigatórios.");
} else if (isCriptomoeda && ticker.isEmpty()) {
CustomToast.info(requireContext(), "Por favor, preencha o Símbolo da Criptomoeda.");
} else {
Ativos ativos = new Ativos(nome, quantidade, Double.parseDouble(precoCompra), Double.parseDouble(precoCompra), dataCompra, tipo, ticker);
// Aqui seria a lógica para guardar na Firebase
DatabaseReference databaseReference = FirebaseDatabase.getInstance().getReference();
String userId = FirebaseAuth.getInstance().getCurrentUser().getUid();
@@ -209,7 +299,7 @@ public class AtivosFragment extends Fragment {
dialog.dismiss();
Toast.makeText(requireContext(), "Ativo adicionado com sucesso!", Toast.LENGTH_SHORT).show();
CustomToast.success(requireContext(), "Ativo adicionado com sucesso!");
}
});

View File

@@ -2,20 +2,61 @@ package com.example.lifegrid.menu;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import com.example.lifegrid.utils.CustomToast;
import com.example.lifegrid.R;
import com.example.lifegrid.models.Transacao;
import com.github.mikephil.charting.charts.BarChart;
import com.github.mikephil.charting.charts.PieChart;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.data.BarData;
import com.github.mikephil.charting.data.BarDataSet;
import com.github.mikephil.charting.data.BarEntry;
import com.github.mikephil.charting.data.PieData;
import com.github.mikephil.charting.data.PieDataSet;
import com.github.mikephil.charting.data.PieEntry;
import com.github.mikephil.charting.formatter.IndexAxisValueFormatter;
import com.github.mikephil.charting.utils.ColorTemplate;
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 java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import android.graphics.Color;
import com.example.lifegrid.TelaInicialActivity;
/**
* 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.
* componente puramente matemática exibida de forma ilustrativa. Trata da visão de Dashboard de estatística.
*/
public class GraficosFragment extends Fragment {
private PieChart pieChartMensal;
private BarChart barChartAnual;
private TextView tvTituloMensal;
private TextView tvTituloAnual;
private List<Transacao> allTransacoesList = new ArrayList<>();
private int currentMes = -1;
private String currentAno = "";
private String currentMesNome = "";
public GraficosFragment() {
// Construtor público vazio obrigatório
}
@@ -25,13 +66,198 @@ public class GraficosFragment extends Fragment {
super.onCreate(savedInstanceState);
}
@Override
public void onResume() {
super.onResume();
if (getActivity() instanceof TelaInicialActivity) {
((TelaInicialActivity) getActivity()).atualizarDadosHome();
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflaciona o layout para este fragmento
View root = inflater.inflate(R.layout.fragment_graficos, container, false);
pieChartMensal = root.findViewById(R.id.pieChartMensal);
barChartAnual = root.findViewById(R.id.barChartAnual);
tvTituloMensal = root.findViewById(R.id.tvTituloMensal);
tvTituloAnual = root.findViewById(R.id.tvTituloAnual);
configurarGraficos();
carregarTransacoes();
return root;
}
private void configurarGraficos() {
// Config PieChart
pieChartMensal.getDescription().setEnabled(false);
pieChartMensal.setUsePercentValues(true);
pieChartMensal.setEntryLabelColor(Color.BLACK);
pieChartMensal.setEntryLabelTextSize(12f);
pieChartMensal.setCenterText("Despesas");
pieChartMensal.setCenterTextSize(18f);
// Config BarChart
barChartAnual.getDescription().setEnabled(false);
barChartAnual.setDrawGridBackground(false);
barChartAnual.getAxisRight().setEnabled(false);
XAxis xAxis = barChartAnual.getXAxis();
xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
xAxis.setGranularity(1f);
xAxis.setCenterAxisLabels(true);
String[] mesesLabel = new String[]{"Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez"};
xAxis.setValueFormatter(new IndexAxisValueFormatter(mesesLabel));
}
private void carregarTransacoes() {
FirebaseUser currentUser = FirebaseAuth.getInstance().getCurrentUser();
if (currentUser == null) return;
String userId = currentUser.getUid();
DatabaseReference databaseReference = FirebaseDatabase.getInstance().getReference();
databaseReference.child("users").child(userId).child("transacoes").addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot snapshot) {
allTransacoesList.clear();
for (DataSnapshot ds : snapshot.getChildren()) {
Transacao t = ds.getValue(Transacao.class);
if (t != null) {
t.setId(ds.getKey());
allTransacoesList.add(t);
}
}
atualizarGraficos();
}
@Override
public void onCancelled(@NonNull DatabaseError error) {
CustomToast.error(requireContext(), "Erro ao carregar transações.");
}
});
}
public void setFiltro(int mesSelecionado, String anoSelecionado, String mesNome) {
this.currentMes = mesSelecionado;
this.currentAno = anoSelecionado;
this.currentMesNome = mesNome;
if (tvTituloMensal != null) {
tvTituloMensal.setText("Resumo de " + mesNome + " " + anoSelecionado);
}
}
if (tvTituloAnual != null) {
tvTituloAnual.setText("Balanço Anual - " + anoSelecionado);
}
atualizarGraficos();
}
private void atualizarGraficos() {
if (currentMes == -1 || currentAno.isEmpty()) return;
atualizarPieChart();
atualizarBarChart();
}
private void atualizarPieChart() {
Map<String, Float> categoriasMap = new HashMap<>();
for (Transacao t : allTransacoesList) {
if (t.getData() != null && t.getTipo() != null && t.getTipo().equalsIgnoreCase("Despesa")) {
String[] parts = t.getData().split("/");
if (parts.length == 3) {
try {
int mesTransacao = Integer.parseInt(parts[1]);
String anoTransacao = parts[2];
if (mesTransacao == currentMes && anoTransacao.equals(currentAno)) {
float valor = Float.parseFloat(t.getValor());
String cat = t.getCategoria();
categoriasMap.put(cat, categoriasMap.getOrDefault(cat, 0f) + valor);
}
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
}
}
List<PieEntry> entries = new ArrayList<>();
for (Map.Entry<String, Float> entry : categoriasMap.entrySet()) {
if (entry.getValue() > 0) {
entries.add(new PieEntry(entry.getValue(), entry.getKey()));
}
}
PieDataSet dataSet = new PieDataSet(entries, "");
dataSet.setColors(ColorTemplate.MATERIAL_COLORS);
dataSet.setSliceSpace(3f);
dataSet.setValueTextSize(14f);
dataSet.setValueTextColor(Color.WHITE);
PieData data = new PieData(dataSet);
pieChartMensal.setData(data);
pieChartMensal.invalidate(); // refresh
}
private void atualizarBarChart() {
float[] receitasPorMes = new float[12];
float[] despesasPorMes = new float[12];
for (Transacao t : allTransacoesList) {
if (t.getData() != null) {
String[] parts = t.getData().split("/");
if (parts.length == 3) {
try {
int mesTransacao = Integer.parseInt(parts[1]) - 1; // 0 a 11
String anoTransacao = parts[2];
if (anoTransacao.equals(currentAno) && mesTransacao >= 0 && mesTransacao < 12) {
float valor = Float.parseFloat(t.getValor());
if (t.getTipo() != null && t.getTipo().equalsIgnoreCase("Receita")) {
receitasPorMes[mesTransacao] += valor;
} else {
despesasPorMes[mesTransacao] += valor;
}
}
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
}
}
List<BarEntry> entriesReceitas = new ArrayList<>();
List<BarEntry> entriesDespesas = new ArrayList<>();
for (int i = 0; i < 12; i++) {
entriesReceitas.add(new BarEntry(i, receitasPorMes[i]));
entriesDespesas.add(new BarEntry(i, despesasPorMes[i]));
}
BarDataSet setReceitas = new BarDataSet(entriesReceitas, "Receitas");
setReceitas.setColor(Color.parseColor("#2ECC71"));
setReceitas.setValueTextSize(10f);
BarDataSet setDespesas = new BarDataSet(entriesDespesas, "Despesas");
setDespesas.setColor(Color.parseColor("#E74C3C"));
setDespesas.setValueTextSize(10f);
BarData data = new BarData(setReceitas, setDespesas);
// agrupar barras
float groupSpace = 0.4f;
float barSpace = 0.05f;
float barWidth = 0.25f;
data.setBarWidth(barWidth);
barChartAnual.setData(data);
barChartAnual.groupBars(0f, groupSpace, barSpace);
barChartAnual.getXAxis().setAxisMinimum(0f);
barChartAnual.getXAxis().setAxisMaximum(0f + barChartAnual.getBarData().getGroupWidth(groupSpace, barSpace) * 12);
barChartAnual.invalidate(); // refresh
}
}

View File

@@ -10,9 +10,11 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import com.example.lifegrid.utils.CustomToast;
import com.example.lifegrid.R;
import com.example.lifegrid.models.Transacao;
import com.example.lifegrid.models.Ativos;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.database.DataSnapshot;
@@ -73,9 +75,74 @@ public class HomeFragment extends Fragment {
.commit();
});
root.findViewById(R.id.ativosCardView).setOnClickListener(v -> {
Fragment ativosFragment = new AtivosFragment();
requireActivity().getSupportFragmentManager().beginTransaction()
.replace(R.id.fragmentContainerView, ativosFragment)
.commit();
});
carregarDadosAtivos();
return root;
}
private void carregarDadosAtivos() {
FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
if (user == null) return;
DatabaseReference ativosRef = FirebaseDatabase.getInstance().getReference()
.child("users").child(user.getUid()).child("ativos");
ativosRef.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot snapshot) {
if (!isAdded()) return;
double totalInvestido = 0;
double valorTotalPortfolio = 0;
for (DataSnapshot ds : snapshot.getChildren()) {
Ativos ativo = ds.getValue(Ativos.class);
if (ativo != null) {
double qtd = 0;
try {
qtd = Double.parseDouble(ativo.getQuantidade().replace(",", "."));
} catch (Exception e) {}
totalInvestido += qtd * ativo.getPrecoCompra();
valorTotalPortfolio += qtd * ativo.getPrecoAtual();
}
}
if (tvValor4 != null) {
tvValor4.setText(String.format(Locale.getDefault(), "%.2f€", valorTotalPortfolio));
}
if (tvTransacoes4 != null) {
double roi = valorTotalPortfolio - totalInvestido;
double roiPct = 0;
if (totalInvestido > 0) {
roiPct = (roi / totalInvestido) * 100;
}
if (roi >= 0) {
tvTransacoes4.setText(String.format(Locale.getDefault(), "+%.2f%% ROI", roiPct));
tvTransacoes4.setTextColor(android.graphics.Color.parseColor("#22C55E"));
} else {
tvTransacoes4.setText(String.format(Locale.getDefault(), "%.2f%% ROI", roiPct));
tvTransacoes4.setTextColor(android.graphics.Color.parseColor("#FF0000"));
}
}
}
@Override
public void onCancelled(@NonNull DatabaseError error) {
// Ignore
}
});
}
@Override
public void onResume() {
super.onResume();
@@ -152,7 +219,7 @@ public class HomeFragment extends Fragment {
@Override
public void onCancelled(@NonNull DatabaseError error) {
if (isAdded()) {
Toast.makeText(getContext(), "Erro ao carregar dados", Toast.LENGTH_SHORT).show();
CustomToast.error(requireContext(), "Erro ao carregar dados");
}
}
});

View File

@@ -16,6 +16,7 @@ import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.example.lifegrid.utils.CustomToast;
import java.util.Calendar;
import java.util.Locale;
@@ -87,7 +88,7 @@ public class MetasFragment extends Fragment {
.setPositiveButton("Sim", (dialog, which) -> {
if (databaseReference != null && userId != null && meta.getId() != null) {
databaseReference.child("users").child(userId).child("metas").child(meta.getId()).removeValue();
Toast.makeText(requireContext(), "Meta excluída.", Toast.LENGTH_SHORT).show();
CustomToast.success(requireContext(), "Meta excluída.");
}
})
.setNegativeButton("Não", null)
@@ -117,8 +118,17 @@ public class MetasFragment extends Fragment {
databaseReference.child("users").child(userId).child("transacoes").child(key).setValue(transacao);
}
double targetVal = 0;
try {
targetVal = Double.parseDouble(meta.getValor().replace(",", "."));
} catch (Exception ignored) {}
editText.setText("");
Toast.makeText(requireContext(), "Valor adicionado e despesa registada!", Toast.LENGTH_SHORT).show();
if (targetVal > 0 && novoValor >= targetVal) {
CustomToast.success(requireContext(), "Parabéns! Alcançou a sua meta: " + meta.getNome() + "!");
} else {
CustomToast.success(requireContext(), "Valor adicionado e despesa registada!");
}
}
}
});
@@ -161,7 +171,7 @@ public class MetasFragment extends Fragment {
@Override
public void onCancelled(@NonNull DatabaseError error) {
Toast.makeText(requireContext(), "Erro ao carregar metas.", Toast.LENGTH_SHORT).show();
CustomToast.error(requireContext(), "Erro ao carregar metas.");
}
});
}
@@ -212,7 +222,7 @@ public class MetasFragment extends Fragment {
if (nome.isEmpty() || categoria.isEmpty() || valor.isEmpty() || data.isEmpty()) {
Toast.makeText(requireContext(), "Por favor, preencha todos os campos.", Toast.LENGTH_SHORT).show();
CustomToast.info(requireContext(), "Por favor, preencha todos os campos.");
} else {
// Aqui seria a lógica para guardar na Firebase
DatabaseReference databaseReference = FirebaseDatabase.getInstance().getReference();
@@ -223,7 +233,7 @@ public class MetasFragment extends Fragment {
dialog.dismiss();
Toast.makeText(requireContext(), "Meta criada com sucesso!", Toast.LENGTH_SHORT).show();
CustomToast.success(requireContext(), "Meta criada com sucesso!");
}
});

View File

@@ -20,6 +20,7 @@ import android.widget.ImageView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import com.example.lifegrid.utils.CustomToast;
import java.util.Calendar;
import java.util.Locale;
@@ -108,7 +109,7 @@ public class TransacoesFragment extends Fragment {
.setPositiveButton("Sim", (dialog, which) -> {
if (databaseReference != null && userId != null && transacao.getId() != null) {
databaseReference.child("users").child(userId).child("transacoes").child(transacao.getId()).removeValue();
Toast.makeText(requireContext(), "Transação excluída.", Toast.LENGTH_SHORT).show();
CustomToast.success(requireContext(), "Transação excluída.");
}
})
.setNegativeButton("Não", null)
@@ -144,7 +145,7 @@ public class TransacoesFragment extends Fragment {
@Override
public void onCancelled(@NonNull DatabaseError error) {
Toast.makeText(requireContext(), "Erro ao carregar transações.", Toast.LENGTH_SHORT).show();
CustomToast.error(requireContext(), "Erro ao carregar transações.");
}
});
}
@@ -295,7 +296,7 @@ public class TransacoesFragment extends Fragment {
if (valor.isEmpty() || descricao.isEmpty() || data.isEmpty() || spinnerCategoria.getSelectedItemPosition() == 0) {
Toast.makeText(requireContext(), "Por favor, preencha todos os campos.", Toast.LENGTH_SHORT).show();
CustomToast.info(requireContext(), "Por favor, preencha todos os campos.");
} else {
DatabaseReference databaseReference = FirebaseDatabase.getInstance().getReference();
String userId = FirebaseAuth.getInstance().getCurrentUser().getUid();
@@ -305,7 +306,7 @@ public class TransacoesFragment extends Fragment {
dialog.dismiss();
Toast.makeText(requireContext(), "Transação adicionada com sucesso!", Toast.LENGTH_SHORT).show();
CustomToast.success(requireContext(), "Transação adicionada com sucesso!");
}
});

View File

@@ -7,18 +7,20 @@ public class Ativos {
private double precoAtual;
private String dataCompra;
private String tipo;
private String ticker;
public Ativos() {
}
public Ativos(String nome, String quantidade, double precoCompra, double precoAtual, String dataCompra, String tipo) {
public Ativos(String nome, String quantidade, double precoCompra, double precoAtual, String dataCompra, String tipo, String ticker) {
this.nome = nome;
this.quantidade = quantidade;
this.precoCompra = precoCompra;
this.precoAtual = precoAtual;
this.dataCompra = dataCompra;
this.tipo = tipo;
this.ticker = ticker;
}
public String getNome() {
@@ -68,4 +70,12 @@ public class Ativos {
public void setTipo(String tipo) {
this.tipo = tipo;
}
public String getTicker() {
return ticker;
}
public void setTicker(String ticker) {
this.ticker = ticker;
}
}

View File

@@ -0,0 +1,67 @@
package com.example.lifegrid.utils;
import android.content.Context;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.example.lifegrid.R;
public class CustomToast {
public enum Type {
SUCCESS,
ERROR,
INFO
}
public static void show(Context context, String message, Type type) {
if (context == null) return;
try {
LayoutInflater inflater = LayoutInflater.from(context);
View layout = inflater.inflate(R.layout.custom_toast, null);
ImageView iconView = layout.findViewById(R.id.toast_icon);
TextView textView = layout.findViewById(R.id.toast_text);
textView.setText(message);
switch (type) {
case SUCCESS:
iconView.setImageResource(R.drawable.ic_toast_success);
break;
case ERROR:
iconView.setImageResource(R.drawable.ic_toast_error);
break;
case INFO:
default:
iconView.setImageResource(R.drawable.ic_toast_info);
break;
}
Toast toast = new Toast(context.getApplicationContext());
toast.setDuration(Toast.LENGTH_SHORT);
toast.setView(layout);
toast.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, 150);
toast.show();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void success(Context context, String message) {
show(context, message, Type.SUCCESS);
}
public static void error(Context context, String message) {
show(context, message, Type.ERROR);
}
public static void info(Context context, String message) {
show(context, message, Type.INFO);
}
}

View File

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

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#E74C3C"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2V7h2v6z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#3498DB"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2V7h2v2z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#2ECC71"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
</vector>

View File

@@ -8,6 +8,19 @@
android:layout_height="match_parent"
tools:context=".CriarContaActivity">
<ImageView
android:id="@+id/btnBack"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:src="@drawable/diagonalarrowleftdownoutline_110924"
app:tint="@color/preto"
android:rotation="45"
android:padding="4dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/logoImageView"
android:layout_width="213dp"
@@ -119,31 +132,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/passwordEditText2" />
<TextView
android:id="@+id/ouTextView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="22dp"
android:text="_____________________ OU _____________________"
android:textColor="@color/cinzaescuro"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginButton2" />
<Button
android:id="@+id/googleButton2"
android:layout_width="316dp"
android:layout_height="49dp"
android:layout_marginTop="14dp"
android:backgroundTint="#FFFFFF"
android:text="Continuar com o Google"
android:textColor="#0F0E0E"
app:cornerRadius="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ouTextView2"
app:strokeColor="@color/cinza"
app:strokeWidth="1sp" />
<TextView
android:id="@+id/textView"
@@ -205,6 +194,6 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/googleButton2" />
app:layout_constraintTop_toBottomOf="@+id/loginButton2" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -149,33 +149,6 @@
android:checked="true" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#E0E0E0"
android:layout_marginVertical="12dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:text="Moeda Base"
android:textColor="@color/preto"
android:textSize="16sp" />
<Spinner
android:id="@+id/spinnerCurrency"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:background="@drawable/rounded_input_bg"
android:entries="@array/moedas_array" />
</LinearLayout>
</LinearLayout>
<!-- Extras Section -->
@@ -196,21 +169,6 @@
android:padding="16dp"
android:layout_marginBottom="32dp">
<TextView
android:id="@+id/tvDocuments"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Meus Documentos (Faturas)"
android:textColor="@color/preto"
android:textSize="16sp"
android:textStyle="bold"
android:paddingVertical="8dp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#E0E0E0"
android:layout_marginVertical="4dp" />
<TextView
android:id="@+id/tvTerms"
@@ -259,6 +217,7 @@
android:layout_width="match_parent"
android:layout_height="55dp"
android:backgroundTint="@color/preto"
android:textColor="@color/branco"
android:text="Guardar Alterações"
android:textSize="16sp"
app:cornerRadius="14dp"

View File

@@ -62,33 +62,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/passesquecerTextView" />
<Button
android:id="@+id/googleButton"
android:layout_width="316dp"
android:layout_height="49dp"
android:layout_marginTop="8dp"
android:backgroundTint="#FFFFFF"
android:text="Continuar com o Google"
android:textColor="#0F0E0E"
app:cornerRadius="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.483"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginButton"
app:strokeColor="@color/cinza"
app:strokeWidth="1sp" />
<TextView
android:id="@+id/ouTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="_____________________ OU _____________________"
android:textColor="@color/cinzaescuro"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.492"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/googleButton" />
<EditText
android:id="@+id/passwordEditText"
@@ -156,7 +130,7 @@
android:textColor="#121111"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ouTextView" />
app:layout_constraintTop_toBottomOf="@+id/loginButton" />
<ProgressBar
android:id="@+id/loadingProgressBar"

View File

@@ -8,6 +8,19 @@
android:layout_height="match_parent"
tools:context=".RecupearPasswordActivity">
<ImageView
android:id="@+id/btnBack"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:src="@drawable/diagonalarrowleftdownoutline_110924"
app:tint="@color/preto"
android:rotation="45"
android:padding="4dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/loginButton3"
android:layout_width="315dp"

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/branco"
tools:context=".TermosPoliticasActivity">
<!-- Header -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/headerLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/btnBack"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/diagonalarrowleftdownoutline_110924"
app:tint="@color/preto"
android:rotation="45"
android:padding="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:contentDescription="Voltar" />
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Documento"
android:textColor="@color/preto"
android:textSize="22sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Scrollable content area -->
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/headerLayout">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:id="@+id/tvContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/preto"
android:textSize="15sp"
android:lineSpacingMultiplier="1.25"
android:linksClickable="true"
android:autoLink="all" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginHorizontal="24dp"
android:background="@drawable/custom_toast_bg"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="16dp"
android:paddingVertical="10dp">
<ImageView
android:id="@+id/toast_icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="10dp"
android:contentDescription="Toast status icon" />
<TextView
android:id="@+id/toast_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxWidth="260dp"
android:textColor="#FFFFFF"
android:textSize="14sp"
android:fontFamily="sans-serif-medium" />
</LinearLayout>

View File

@@ -51,6 +51,28 @@
android:paddingHorizontal="16dp"
android:textColor="@color/preto" />
<TextView
android:id="@+id/tvTickerLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="Símbolo / Ticker (Ex: BTC-USD)"
android:textColor="@color/preto"
android:textStyle="bold"
android:visibility="gone" />
<EditText
android:id="@+id/etTickerAtivo"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginBottom="16dp"
android:background="@drawable/rounded_input_bg"
android:hint="BTC-USD, ETH-USD"
android:inputType="textCapCharacters"
android:paddingHorizontal="16dp"
android:textColor="@color/preto"
android:visibility="gone" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -92,7 +114,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="Preço de Compra (€)"
android:text="Valor Investido (€)"
android:textColor="@color/preto"
android:textStyle="bold" />

View File

@@ -7,7 +7,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/adicionarCardView"
android:layout_width="364dp"
android:layout_width="391dp"
android:layout_height="match_parent"
android:layout_marginTop="544dp"
android:background="@drawable/cardview_background"
@@ -85,8 +85,8 @@
android:id="@+id/textView14"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="36dp"
android:layout_marginTop="36dp"
android:layout_marginStart="24dp"
android:layout_marginTop="32dp"
android:fontFamily="sans-serif"
android:text="Valor Total do Portfólio"
android:textColor="#4A5568"
@@ -98,8 +98,8 @@
android:id="@+id/textView17"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="36dp"
android:layout_marginBottom="36dp"
android:layout_marginStart="24dp"
android:layout_marginBottom="40dp"
android:fontFamily="sans-serif-medium"
android:text="0.00€"
android:textColor="#1A202C"

View File

@@ -1,14 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
tools:context=".menu.GraficosFragment">
<!-- TODO: Update blank fragment layout -->
<TextView
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/hello_blank_fragment" />
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="100dp"
android:paddingTop="16dp">
</FrameLayout>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="16dp"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
android:backgroundTint="#FFFFFF">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tvTituloMensal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Resumo Mensal"
android:textColor="#1A202C"
android:textSize="18sp"
android:textStyle="bold" />
<com.github.mikephil.charting.charts.PieChart
android:id="@+id/pieChartMensal"
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_marginTop="16dp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="16dp"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
android:backgroundTint="#FFFFFF">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tvTituloAnual"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Balanço Anual"
android:textColor="#1A202C"
android:textSize="18sp"
android:textStyle="bold" />
<com.github.mikephil.charting.charts.BarChart
android:id="@+id/barChartAnual"
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_marginTop="16dp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>

View File

@@ -247,7 +247,7 @@
android:layout_marginStart="7dp"
android:layout_marginTop="12dp"
android:fontFamily="sans-serif"
android:text="Valor do Ativos"
android:text="Valor de Ativos"
android:textColor="#4A5568"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"

View File

@@ -12,12 +12,11 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/adicionarCardView"
android:layout_width="336dp"
android:layout_width="367dp"
android:layout_height="match_parent"
android:layout_marginTop="15dp"
android:background="@drawable/cardview_background"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.432"
app:layout_constraintHorizontal_bias="0.493"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
@@ -28,8 +27,8 @@
android:layout_marginTop="48dp"
android:fontFamily="sans-serif"
android:text="Metas Financeiras"
android:textSize="19sp"
android:textColor="@color/preto"
android:textSize="19sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.093"
app:layout_constraintStart_toStartOf="parent"
@@ -66,13 +65,13 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvMetas"
android:layout_width="327dp"
android:layout_height="match_parent"
android:layout_width="313dp"
android:layout_height="597dp"
android:layout_marginStart="16dp"
android:layout_marginTop="100dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.478"
app:layout_constraintHorizontal_bias="0.444"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/novaTransacaoButton" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -54,18 +54,19 @@
tools:text="Criptomoedas" />
<!-- Quantity and Purchase Price -->
<!-- Delete Button -->
<TextView
android:id="@+id/tvInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginTop="12dp"
android:textColor="#6b7280"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="@id/tvNome"
app:layout_constraintTop_toBottomOf="@id/tvTipo"
tools:text="12 unidades x 16.10€" />
<!-- Delete Button -->
<ImageView
android:id="@+id/btnDelete"
android:layout_width="28dp"
@@ -111,6 +112,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:layout_marginEnd="8dp"
android:textColor="#16a34a"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"

View File

@@ -43,4 +43,52 @@
<item>USD ($)</item>
<item>BRL (R$)</item>
</string-array>
<string name="terms_title">Termos e Condições</string>
<string name="terms_content"><![CDATA[
<h3><b>1. Introdução</b></h3>
<p>Bem-vindo ao <b>LifeGrid</b>. Ao descarregar, instalar ou utilizar a nossa aplicação, o utilizador concorda e aceita cumprir integralmente os presentes Termos e Condições de Serviço.</p>
<h3><b>2. Descrição do Serviço</b></h3>
<p>O LifeGrid é uma aplicação de gestão de finanças pessoais concebida para ajudar os utilizadores a registar e categorizar transações financeiras, gerir e monitorizar ativos, estabelecer metas financeiras e guardar faturas ou outros documentos comprovativos associados às suas finanças.</p>
<h3><b>3. Registo e Segurança da Conta</b></h3>
<p>Para usufruir de todas as funcionalidades da aplicação, é necessária a criação de uma conta de utilizador através de um endereço de correio eletrónico válido e uma palavra-passe. O utilizador é o único responsável por manter a confidencialidade das suas credenciais de acesso, bem como por qualquer atividade realizada sob a sua conta.</p>
<h3><b>4. Propriedade Intelectual</b></h3>
<p>Todo o design, código-fonte, elementos gráficos, logótipos e conteúdos da aplicação LifeGrid são propriedade exclusiva da equipa de desenvolvimento e protegidos por leis de direitos de autor nacionais e internacionais.</p>
<h3><b>5. Uso Aceitável e Proibições</b></h3>
<p>O utilizador compromete-se a não utilizar o LifeGrid para qualquer fim ilegal, fraudulento ou não autorizado. É estritamente proibido interferir com a segurança dos servidores e bases de dados da infraestrutura (Firebase), ou tentar contornar quaisquer mecanismos de segurança da aplicação.</p>
<h3><b>6. Limitação de Responsabilidade</b></h3>
<p>O LifeGrid é fornecido "tal como está" e "conforme disponível". Não garantimos que a aplicação funcione de forma ininterrupta ou isenta de erros. As decisões financeiras tomadas com base nas informações apresentadas na aplicação são da inteira e exclusiva responsabilidade do utilizador. O LifeGrid não constitui ou substitui qualquer aconselhamento financeiro profissional.</p>
<h3><b>7. Alterações aos Termos</b></h3>
<p>Reservamo-nos o direito de alterar ou atualizar estes Termos e Condições a qualquer momento. Quaisquer atualizações serão publicadas nesta secção e o uso continuado da aplicação constitui a aceitação tácita das novas condições.</p>
]]></string>
<string name="privacy_title">Política de Privacidade</string>
<string name="privacy_content"><![CDATA[
<h3><b>1. Informações que Recolhemos</b></h3>
<p>Recolhemos os dados que nos faculta ativamente ao registar-se e utilizar a aplicação. Estes incluem:</p>
<ul>
<li>Dados de Registo: Nome de utilizador e endereço de correio eletrónico.</li>
<li>Dados Financeiros: Transações registadas, montantes, categorias, ativos introduzidos e metas estabelecidas.</li>
<li>Documentos: Faturas e outros comprovativos fotográficos que decida associar às suas transações.</li>
</ul>
<h3><b>2. Finalidade do Tratamento de Dados</b></h3>
<p>Os dados recolhidos destinam-se única e exclusivamente a permitir o funcionamento adequado da aplicação LifeGrid, permitindo-lhe gerir a sua carteira, analisar gráficos de despesas/receitas, acompanhar o progresso de metas e guardar comprovativos digitais.</p>
<h3><b>3. Armazenamento e Segurança dos Dados</b></h3>
<p>Para garantir o armazenamento seguro dos seus dados, o LifeGrid utiliza a plataforma cloud <b>Google Firebase</b> (incluindo Firebase Authentication para gestão de identidade, Firebase Realtime Database para sincronização de dados e Firebase Storage para armazenamento de imagens). Implementamos práticas de segurança adequadas para prevenir o acesso não autorizado, alteração ou eliminação dos seus dados.</p>
<h3><b>4. Partilha de Dados com Terceiros</b></h3>
<p>O LifeGrid não partilha, vende, aluga ou comercializa os seus dados pessoais com terceiros para quaisquer fins comerciais ou de marketing. Os dados são transferidos apenas para os servidores do Firebase para efeitos estritos de alojamento e sincronização.</p>
<h3><b>5. Direitos do Utilizador</b></h3>
<p>O utilizador tem controlo total sobre os seus dados, podendo adicionar, editar ou apagar as suas transações, faturas e detalhes de conta a qualquer momento. Se desejar eliminar permanentemente a sua conta e todos os dados associados dos nossos servidores, pode fazê-lo contactando-nos ou solicitando a eliminação.</p>
<h3><b>6. Alterações a esta Política</b></h3>
<p>Podemos atualizar esta Política de Privacidade periodicamente para refletir mudanças nas nossas práticas ou por razões operacionais/legais. Recomendamos a consulta regular desta secção para se manter informado.</p>
]]></string>
</resources>

View File

@@ -5,5 +5,22 @@
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
<style name="Theme.LifeGrid" parent="Base.Theme.LifeGrid" />
<style name="Theme.LifeGrid" parent="Base.Theme.LifeGrid">
<item name="android:spinnerItemStyle">@style/SpinnerItemStyle</item>
<item name="android:spinnerDropDownItemStyle">@style/SpinnerDropDownItemStyle</item>
<item name="android:spinnerStyle">@style/SpinnerStyle</item>
<item name="spinnerStyle">@style/SpinnerStyle</item>
</style>
<style name="SpinnerStyle" parent="@android:style/Widget.Spinner">
<item name="android:popupBackground">@color/branco</item>
</style>
<style name="SpinnerItemStyle" parent="@android:style/Widget.TextView.SpinnerItem">
<item name="android:textColor">@color/preto</item>
</style>
<style name="SpinnerDropDownItemStyle" parent="@android:style/Widget.DropDownItem.Spinner">
<item name="android:textColor">@color/preto</item>
</style>
</resources>

37
fluxograma_aplicacao.md Normal file
View File

@@ -0,0 +1,37 @@
# Sistemas e Aplicações da LifeGrid (Super Simplificado)
Este diagrama mostra apenas as aplicações, serviços e bases de dados externas utilizadas no funcionamento do **LifeGrid** e como a informação passa entre elas.
## Fluxo de Informação entre Serviços (Mermaid)
```mermaid
flowchart LR
%% Aplicação Central
App[Aplicação Android <br/> LifeGrid]
%% Serviços de Autenticação
App <-->|1. Autenticação e Sessão| Auth[Firebase Authentication]
%% Inteligência Artificial
App -->|2. Imagem da Fatura| Gemini[Gemini API <br/> Google AI SDK]
Gemini -->|Dados Extraídos| App
%% Alojamento de Imagens
App -->|3. Ficheiro de Fatura| Storage[Firebase Storage]
Storage -->|URL do Ficheiro| App
%% Base de Dados
App <-->|4. Transações, Ativos e Metas| Database[Firebase Realtime Database]
%% Estilos simples com fundo branco
classDef whiteBackground fill:#FFFFFF,stroke:#000000,stroke-width:1.5dp,color:#000000;
class App,Auth,Gemini,Storage,Database whiteBackground;
```
---
### Como a informação passa:
1. **Firebase Authentication**: Controla o acesso à aplicação (Login/Registo).
2. **Gemini API (Google AI)**: Recebe a imagem da fatura enviada pela app e devolve os dados estruturados (valor, data, descrição).
3. **Firebase Storage**: Recebe e armazena os ficheiros de imagem das faturas digitalizadas.
4. **Firebase Realtime Database**: Guarda e sincroniza em tempo real todos os dados financeiros (saldos, metas, ativos e referências dos documentos).

View File

@@ -19,6 +19,7 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}