diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e3cbbcf..d9f2581 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index acb3ccd..45d35d5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,6 +13,9 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.LifeGrid"> + diff --git a/app/src/main/java/com/example/lifegrid/DefinicoesActivity.java b/app/src/main/java/com/example/lifegrid/DefinicoesActivity.java index 2860e2b..6497fc9 100644 --- a/app/src/main/java/com/example/lifegrid/DefinicoesActivity.java +++ b/app/src/main/java/com/example/lifegrid/DefinicoesActivity.java @@ -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(); }); diff --git a/app/src/main/java/com/example/lifegrid/DocumentosActivity.java b/app/src/main/java/com/example/lifegrid/DocumentosActivity.java new file mode 100644 index 0000000..3a231da --- /dev/null +++ b/app/src/main/java/com/example/lifegrid/DocumentosActivity.java @@ -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); + } + } +} diff --git a/app/src/main/java/com/example/lifegrid/InvoiceScannerHelper.java b/app/src/main/java/com/example/lifegrid/InvoiceScannerHelper.java new file mode 100644 index 0000000..c49c21c --- /dev/null +++ b/app/src/main/java/com/example/lifegrid/InvoiceScannerHelper.java @@ -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 response = model.generateContent(content); + + Futures.addCallback(response, new FutureCallback() { + @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()); + } + } +} diff --git a/app/src/main/java/com/example/lifegrid/TelaInicialActivity.java b/app/src/main/java/com/example/lifegrid/TelaInicialActivity.java index 51f4f0b..18d45ad 100644 --- a/app/src/main/java/com/example/lifegrid/TelaInicialActivity.java +++ b/app/src/main/java/com/example/lifegrid/TelaInicialActivity.java @@ -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 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() { diff --git a/app/src/main/java/com/example/lifegrid/menu/TransacoesFragment.java b/app/src/main/java/com/example/lifegrid/menu/TransacoesFragment.java index 93a288d..3ceb86a 100644 --- a/app/src/main/java/com/example/lifegrid/menu/TransacoesFragment.java +++ b/app/src/main/java/com/example/lifegrid/menu/TransacoesFragment.java @@ -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(); diff --git a/app/src/main/res/layout/activity_definicoes.xml b/app/src/main/res/layout/activity_definicoes.xml index 40c9bcb..2951792 100644 --- a/app/src/main/res/layout/activity_definicoes.xml +++ b/app/src/main/res/layout/activity_definicoes.xml @@ -196,6 +196,22 @@ android:padding="16dp" android:layout_marginBottom="32dp"> + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_tela_inicial.xml b/app/src/main/res/layout/activity_tela_inicial.xml index 69aa4b5..7282110 100644 --- a/app/src/main/res/layout/activity_tela_inicial.xml +++ b/app/src/main/res/layout/activity_tela_inicial.xml @@ -229,6 +229,16 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_ativos.xml b/app/src/main/res/layout/fragment_ativos.xml index b8fd40e..0cf8c2d 100644 --- a/app/src/main/res/layout/fragment_ativos.xml +++ b/app/src/main/res/layout/fragment_ativos.xml @@ -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" /> diff --git a/app/src/main/res/layout/fragment_metas.xml b/app/src/main/res/layout/fragment_metas.xml index 30504dc..f6017a4 100644 --- a/app/src/main/res/layout/fragment_metas.xml +++ b/app/src/main/res/layout/fragment_metas.xml @@ -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" diff --git a/app/src/main/res/layout/fragment_transacoes.xml b/app/src/main/res/layout/fragment_transacoes.xml index ebf57a6..dc85f8c 100644 --- a/app/src/main/res/layout/fragment_transacoes.xml +++ b/app/src/main/res/layout/fragment_transacoes.xml @@ -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" />