This commit is contained in:
2026-05-05 16:18:47 +01:00
parent 24f20191a5
commit 8c9bdc551a
13 changed files with 389 additions and 9 deletions

View File

@@ -58,6 +58,8 @@ dependencies {
implementation(libs.ui.tooling.preview)
implementation(libs.material3)
implementation(libs.material3.adaptive.navigation.suite)
implementation("com.google.ai.client.generativeai:generativeai:0.7.0")
implementation("com.google.guava:guava:31.1-android")
testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)

View File

@@ -13,6 +13,9 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.LifeGrid">
<activity
android:name=".DocumentosActivity"
android:exported="false" />
<activity
android:name=".DefinicoesActivity"
android:exported="false" />

View File

@@ -108,6 +108,14 @@ 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);
startActivity(intent);
});
}
btnBack.setOnClickListener(v -> {
finish();
});

View File

@@ -0,0 +1,91 @@
package com.example.lifegrid;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
public class DocumentosActivity extends AppCompatActivity {
private ImageView btnBack;
private LinearLayout llDocumentsList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_documentos);
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);
llDocumentsList = findViewById(R.id.llDocumentsList);
btnBack.setOnClickListener(v -> finish());
loadDocuments();
}
private void loadDocuments() {
SharedPreferences prefs = getSharedPreferences("LifeGridDocs", Context.MODE_PRIVATE);
int count = prefs.getInt("doc_count", 0);
if (count == 0) {
TextView emptyText = new TextView(this);
emptyText.setText("Nenhum documento guardado.");
emptyText.setPadding(32, 32, 32, 32);
llDocumentsList.addView(emptyText);
return;
}
for (int i = 0; i < count; i++) {
String uriString = prefs.getString("doc_uri_" + i, "");
String desc = prefs.getString("doc_desc_" + i, "Sem descrição");
String data = prefs.getString("doc_data_" + i, "Sem data");
LinearLayout itemLayout = new LinearLayout(this);
itemLayout.setOrientation(LinearLayout.HORIZONTAL);
itemLayout.setPadding(16, 16, 16, 16);
ImageView ivDoc = new ImageView(this);
ivDoc.setLayoutParams(new LinearLayout.LayoutParams(200, 200));
ivDoc.setScaleType(ImageView.ScaleType.CENTER_CROP);
try {
ivDoc.setImageURI(Uri.parse(uriString));
} catch (Exception e) {
e.printStackTrace();
}
LinearLayout textLayout = new LinearLayout(this);
textLayout.setOrientation(LinearLayout.VERTICAL);
textLayout.setPadding(16, 0, 0, 0);
TextView tvDesc = new TextView(this);
tvDesc.setText(desc);
tvDesc.setTextSize(16);
tvDesc.setTypeface(null, android.graphics.Typeface.BOLD);
TextView tvData = new TextView(this);
tvData.setText(data);
textLayout.addView(tvDesc);
textLayout.addView(tvData);
itemLayout.addView(ivDoc);
itemLayout.addView(textLayout);
llDocumentsList.addView(itemLayout);
}
}
}

View File

@@ -0,0 +1,92 @@
package com.example.lifegrid;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
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.JSONObject;
import java.io.InputStream;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class InvoiceScannerHelper {
private static final String API_KEY = "YOUR_GEMINI_API_KEY_HERE"; // Substitua pela sua chave API
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 {
void onSuccess(double valor, String descricao, String categoria, String data);
void onError(String error);
}
public static void scanInvoice(Context context, Uri imageUri, ScanCallback callback) {
if (API_KEY.equals("YOUR_GEMINI_API_KEY_HERE")) {
callback.onError("Chave API do Gemini não configurada.");
return;
}
try {
InputStream imageStream = context.getContentResolver().openInputStream(imageUri);
Bitmap bitmap = BitmapFactory.decodeStream(imageStream);
GenerativeModel gm = new GenerativeModel(
"gemini-1.5-flash",
API_KEY
);
GenerativeModelFutures model = GenerativeModelFutures.from(gm);
Content content = new Content.Builder()
.addText(PROMPT)
.addImage(bitmap)
.build();
Executor executor = Executors.newSingleThreadExecutor();
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.");
}
}
@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());
}
}, executor);
} catch (Exception e) {
e.printStackTrace();
callback.onError("Erro ao processar imagem: " + e.getMessage());
}
}
}

View File

@@ -59,6 +59,9 @@ public class TelaInicialActivity extends AppCompatActivity {
private String[] meses = {"Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro"};
private androidx.activity.result.ActivityResultLauncher<android.net.Uri> takePictureLauncher;
private android.net.Uri currentPhotoUri;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -70,7 +73,18 @@ public class TelaInicialActivity extends AppCompatActivity {
return insets;
});
takePictureLauncher = registerForActivityResult(
new androidx.activity.result.contract.ActivityResultContracts.TakePicture(),
success -> {
if (success) {
processInvoiceImage(currentPhotoUri);
}
}
);
com.google.android.material.floatingactionbutton.FloatingActionButton fabScanInvoice = findViewById(R.id.fabScanInvoice);
fabScanInvoice.setOnClickListener(v -> startInvoiceScan());
ivHeaderProfilePicture = findViewById(R.id.ivHeaderProfilePicture);
tvHeaderUsername = findViewById(R.id.tvHeaderUsername);
@@ -141,6 +155,55 @@ public class TelaInicialActivity extends AppCompatActivity {
startActivity(intent);
});
}
public void startInvoiceScan() {
java.io.File photoFile = new java.io.File(getFilesDir(), "invoice_" + System.currentTimeMillis() + ".jpg");
currentPhotoUri = androidx.core.content.FileProvider.getUriForFile(this, getPackageName() + ".fileprovider", photoFile);
takePictureLauncher.launch(currentPhotoUri);
}
private void processInvoiceImage(android.net.Uri imageUri) {
Toast.makeText(this, "A processar fatura com IA...", Toast.LENGTH_LONG).show();
InvoiceScannerHelper.scanInvoice(this, imageUri, new InvoiceScannerHelper.ScanCallback() {
@Override
public void onSuccess(double valor, String descricao, String categoria, String data) {
runOnUiThread(() -> {
Fragment currentFragment = getSupportFragmentManager().findFragmentById(R.id.fragmentContainerView);
TransacoesFragment transFragment;
if (currentFragment instanceof TransacoesFragment) {
transFragment = (TransacoesFragment) currentFragment;
} else {
updateNavSelection(findViewById(R.id.carteiraImageView));
transFragment = new TransacoesFragment();
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragmentContainerView, transFragment)
.commit();
getSupportFragmentManager().executePendingTransactions();
}
transFragment.showNovaTransacaoDialog(valor, descricao, categoria, data);
// Save document to shared preferences or database
saveDocument(imageUri.toString(), descricao, data);
});
}
@Override
public void onError(String error) {
runOnUiThread(() -> Toast.makeText(TelaInicialActivity.this, error, Toast.LENGTH_LONG).show());
}
});
}
private void saveDocument(String uriString, String descricao, String data) {
SharedPreferences prefs = getSharedPreferences("LifeGridDocs", Context.MODE_PRIVATE);
int count = prefs.getInt("doc_count", 0);
SharedPreferences.Editor editor = prefs.edit();
editor.putString("doc_uri_" + count, uriString);
editor.putString("doc_desc_" + count, descricao);
editor.putString("doc_data_" + count, data);
editor.putInt("doc_count", count + 1);
editor.apply();
}
@Override
protected void onResume() {

View File

@@ -74,6 +74,15 @@ public class TransacoesFragment extends Fragment {
Button novaTransacaoButton = root.findViewById(R.id.novaTransacaoButton);
novaTransacaoButton.setOnClickListener(v -> showNovaTransacaoDialog());
Button escanearTransacaoButton = root.findViewById(R.id.escanearTransacaoButton);
if (escanearTransacaoButton != null) {
escanearTransacaoButton.setOnClickListener(v -> {
if (getActivity() instanceof com.example.lifegrid.TelaInicialActivity) {
((com.example.lifegrid.TelaInicialActivity) getActivity()).startInvoiceScan();
}
});
}
rvTransacoes = root.findViewById(R.id.rvTransacoes);
tvEmptyState = root.findViewById(R.id.textView13);
@@ -136,11 +145,15 @@ public class TransacoesFragment extends Fragment {
});
}
public void showNovaTransacaoDialog() {
showNovaTransacaoDialog(0.0, "", "", "");
}
/**
* 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() {
public void showNovaTransacaoDialog(double defaultValor, String defaultDescricao, String defaultCategoria, String defaultData) {
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
View dialogView = getLayoutInflater().inflate(R.layout.dialog_nova_transacao, null);
builder.setView(dialogView);
@@ -154,6 +167,9 @@ public class TransacoesFragment extends Fragment {
btnFechar.setOnClickListener(v -> dialog.dismiss());
EditText etData = dialogView.findViewById(R.id.etData);
if (!defaultData.isEmpty()) {
etData.setText(defaultData);
}
etData.setOnClickListener(v -> {
Calendar calendar = Calendar.getInstance();
int year = calendar.get(Calendar.YEAR);
@@ -170,7 +186,13 @@ public class TransacoesFragment extends Fragment {
Button btnAdicionarTransacao = dialogView.findViewById(R.id.btnAdicionarTransacao);
EditText etValor = dialogView.findViewById(R.id.etValor);
if (defaultValor > 0) {
etValor.setText(String.valueOf(defaultValor));
}
EditText etDescricao = dialogView.findViewById(R.id.etDescricao);
if (!defaultDescricao.isEmpty()) {
etDescricao.setText(defaultDescricao);
}
Spinner spinnerCategoria = dialogView.findViewById(R.id.spinnerCategoria);
Spinner spinnerTipo = dialogView.findViewById(R.id.spinnerTipo);
@@ -182,12 +204,26 @@ public class TransacoesFragment extends Fragment {
arrayResId, android.R.layout.simple_spinner_item);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerCategoria.setAdapter(adapter);
if (!defaultCategoria.isEmpty()) {
for (int i = 0; i < adapter.getCount(); i++) {
if (adapter.getItem(i).toString().equalsIgnoreCase(defaultCategoria)) {
spinnerCategoria.setSelection(i);
break;
}
}
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
// Se vier da IA (geralmente despesa)
if (!defaultDescricao.isEmpty()) {
spinnerTipo.setSelection(1); // Despesa default para faturas
}
btnAdicionarTransacao.setOnClickListener(v -> {
String valor = etValor.getText().toString().trim();

View File

@@ -196,6 +196,22 @@
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"
android:layout_width="match_parent"

View File

@@ -0,0 +1,58 @@
<?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=".DocumentosActivity">
<!-- 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" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Meus Documentos"
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>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/headerLayout">
<LinearLayout
android:id="@+id/llDocumentsList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp" />
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -229,6 +229,16 @@
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabScanInvoice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
android:src="@android:drawable/ic_menu_camera"
app:backgroundTint="@color/preto"
app:tint="@color/branco"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -21,11 +21,12 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:fontFamily="sans-serif"
android:text="Ativos e Investimentos"
android:textSize="25sp"
android:textStyle="bold"
android:textAlignment="center"
android:textColor="#000000"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.501"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

View File

@@ -28,7 +28,7 @@
android:layout_marginTop="50dp"
android:text="Metas Financeiras"
android:textSize="20sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.147"
app:layout_constraintStart_toStartOf="parent"

View File

@@ -30,9 +30,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:fontFamily="sans-serif"
android:text="Transações"
android:textSize="25sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.501"
app:layout_constraintStart_toStartOf="parent"
@@ -57,7 +57,7 @@
android:layout_marginTop="20dp"
android:layout_marginEnd="25dp"
android:backgroundTint="@color/preto"
android:text="Escanear Fatura"
android:text="Digitalizar Fatura"
app:cornerRadius="10sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView12" />