Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Android CI

on:
pull_request:
push:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- uses: gradle/actions/setup-gradle@v4
with:
gradle-version: '8.9'
- name: Build debug + unit tests
# Usa ./gradlew si el repo tiene wrapper; si no, el gradle instalado por setup-gradle.
run: |
if [ -x ./gradlew ]; then G=./gradlew; else G=gradle; fi
$G assembleDebug testDebugUnitTest --stacktrace
- name: Test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: unit-test-reports
path: app/build/reports/tests/
- name: Debug APK
uses: actions/upload-artifact@v4
with:
name: app-debug
path: app/build/outputs/apk/debug/*.apk
46 changes: 23 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SpreadVE
# Codivisas

App Android 100% nativa (Kotlin + Jetpack Compose) que muestra las tasas oficiales del Banco Central de Venezuela (BCV) y Binance P2P USDT/VES, e incluye convertidor, widgets, notificaciones y utilidades de pago.

Expand All @@ -9,12 +9,12 @@ App Android 100% nativa (Kotlin + Jetpack Compose) que muestra las tasas oficial
- Android SDK 35
- Dispositivo/emulador con API 26+

## Cómo abrir y ejecutar
## Cómo abrir y ejecutar

1. Clona o abre esta carpeta en Android Studio.
2. Espera a que Gradle sincronice las dependencias.
3. Conecta un dispositivo o inicia un emulador.
4. Pulsa **Run** (▶) sobre el módulo `app`.
4. Pulsa **Run** (▶) sobre el módulo `app`.

Desde terminal (con Gradle wrapper):

Expand All @@ -25,7 +25,7 @@ Desde terminal (con Gradle wrapper):

## Fuentes de datos

| Mercado | Fuente | API | Autenticación |
| Mercado | Fuente | API | Autenticación |
|---------|--------|-----|---------------|
| BCV oficial | bcv.today | `GET https://bcv.today/api/v1/rate.json` | No |
| Binance P2P | Binance (interno) | `POST https://p2p.binance.com/bapi/c2c/v2/friendly/c2c/adv/search` | No |
Expand All @@ -38,52 +38,52 @@ Edita `app/src/main/java/com/codeonyx/bcvaqui/data/ApiConfig.kt`:

```kotlin
const val BCV_RATE_URL = "https://bcv.today/api/v1/"
const val WIDGET_UPDATE_INTERVAL_MINUTES = 30L // mínimo WorkManager: 15
const val WIDGET_UPDATE_INTERVAL_MINUTES = 30L // mínimo WorkManager: 15
const val DEFAULT_BACKGROUND_INTERVAL_HOURS = 4L
```

Los intervalos de notificación se configuran desde **Ajustes** en la app (1, 2, 4, 6, 12, 24 h o desactivado).
Los intervalos de notificación se configuran desde **Ajustes** en la app (1, 2, 4, 6, 12, 24 h o desactivado).

## Funcionalidades

### Pantalla principal
- Tasa grande con 2 decimales (`1 USD = 785.07 VES`)
- Selector de moneda (USD, EUR, CNY, TRY, RUB)
- Botón **Copiar tasa**
- Botón **Copiar tasa**
- Convertidor entre monedas BCV y VES
- Mensaje amigable sin internet + última tasa en caché
- Mensaje amigable sin internet + última tasa en caché

### Widget
- Muestra la moneda preferida (`USD: 785.07 VES`)
- Se actualiza cada ~30 min y al abrir la app
- Tap abre la app

### Notificaciones
- Cambios de tasa (subió/bajó) con valor anterior y nuevo
- Alertas de desviación configurable (ej. ±5% vs BCV)
- Cambios de tasa (subió/bajó) con valor anterior y nuevo
- Alertas de desviación configurable (ej. ±5% vs BCV)
- WorkManager en segundo plano

## Estructura del proyecto

```
app/src/main/java/com/codeonyx/bcvaqui/
├── data/
│ ├── ApiConfig.kt # URLs e intervalos
│ ├── api/ # Retrofit clients
│ ├── model/ # DTOs y modelos
│ ├── local/ # DataStore
│ └── repository/ # Capa de datos
├── domain/FxCalculator.kt # Conversión y cálculos
├── ui/ # Compose screens
├── widget/ # Home screen widget
├── worker/ # WorkManager
└── notification/ # Notificaciones locales
├── data/
│ ├── ApiConfig.kt # URLs e intervalos
│ ├── api/ # Retrofit clients
│ ├── model/ # DTOs y modelos
│ ├── local/ # DataStore
│ └── repository/ # Capa de datos
├── domain/FxCalculator.kt # Conversión y cálculos
├── ui/ # Compose screens
├── widget/ # Home screen widget
├── worker/ # WorkManager
└── notification/ # Notificaciones locales
```

## Notas legales

Las tasas BCV provienen de fuentes públicas que reflejan datos del Banco Central de Venezuela. Binance P2P es referencia de mercado, **no oficial**. Esta app es informativa; verifica siempre antes de operaciones financieras.
Las tasas BCV provienen de fuentes públicas que reflejan datos del Banco Central de Venezuela. Binance P2P es referencia de mercado, **no oficial**. Esta app es informativa; verifica siempre antes de operaciones financieras.

## Licencia

Proyecto personal — libre para modificar y usar.
Proyecto personal — libre para modificar y usar.
31 changes: 22 additions & 9 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ android {
applicationId = "com.codeonyx.bcvaqui"
minSdk = 26
targetSdk = 35
versionCode = 6
versionName = "1.3.0"
versionCode = 7
versionName = "1.3.1"
}

signingConfigs {
create("release") {
if (keystorePropertiesFile.exists()) {
if (keystorePropertiesFile.exists()) {
create("release") {
storeFile = rootProject.file(prop("storeFile"))
storePassword = prop("storePassword")
keyAlias = prop("keyAlias")
Expand All @@ -43,11 +43,9 @@ android {
buildTypes {
release {
isMinifyEnabled = true
signingConfig = if (keystorePropertiesFile.exists()) {
signingConfigs.getByName("release")
} else {
signingConfigs.getByName("debug")
}
// Nunca firmar el release con la clave de debug: ese APK no podría
// actualizar el que los usuarios ya tienen instalado.
signingConfig = signingConfigs.findByName("release")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
Expand Down Expand Up @@ -76,6 +74,19 @@ android {
}
}

// Falla con un mensaje claro si se intenta empaquetar el release sin keystore.
gradle.taskGraph.whenReady {
val buildsRelease = allTasks.any { task ->
task.project == project && Regex("^(assemble|bundle|package|sign)Release.*").matches(task.name)
}
if (buildsRelease && !keystorePropertiesFile.exists()) {
throw GradleException(
"Falta keystore.properties en la raíz del proyecto. El release no se firma con la clave " +
"de debug. Copia keystore.properties.example, complétalo y vuelve a compilar."
)
}
}

dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
Expand All @@ -100,4 +111,6 @@ dependencies {
implementation(libs.androidx.splashscreen)

debugImplementation(libs.androidx.compose.ui.tooling.preview)

testImplementation("junit:junit:4.13.2")
}
98 changes: 55 additions & 43 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
<application
android:name=".BcvAquiApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
Expand All @@ -19,18 +21,64 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.BcvAqui.Splash">
android:theme="@style/Theme.BcvAqui.Splash" />

<!-- Íconos por paleta: solo los alias llevan MAIN/LAUNCHER; Azul es el predeterminado. -->
<activity-alias
android:name=".LauncherAzul"
android:enabled="true"
android:exported="true"
android:icon="@mipmap/ic_launcher_azul"
android:roundIcon="@mipmap/ic_launcher_azul_round"
android:label="@string/app_name"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</activity-alias>

<activity
android:name=".widget.ShareRateActivity"
android:excludeFromRecents="true"
android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity-alias
android:name=".LauncherOnix"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_onix"
android:roundIcon="@mipmap/ic_launcher_onix_round"
android:label="@string/app_name"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>

<activity-alias
android:name=".LauncherMorado"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_morado"
android:roundIcon="@mipmap/ic_launcher_morado_round"
android:label="@string/app_name"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>

<activity-alias
android:name=".LauncherEsmeralda"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_esmeralda"
android:roundIcon="@mipmap/ic_launcher_esmeralda_round"
android:label="@string/app_name"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>

<provider
android:name="androidx.core.content.FileProvider"
Expand All @@ -55,18 +103,6 @@
android:resource="@xml/rate_widget_info" />
</receiver>

<receiver
android:name=".widget.MiniRateWidgetProvider"
android:exported="true"
android:label="@string/widget_mini_label">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/mini_rate_widget_info" />
</receiver>

<receiver
android:name=".widget.SpreadWidgetProvider"
android:exported="true"
Expand All @@ -92,30 +128,6 @@
android:resource="@xml/converter_widget_info" />
</receiver>

<receiver
android:name=".widget.VariationWidgetProvider"
android:exported="true"
android:label="@string/widget_variation_label">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/variation_widget_info" />
</receiver>

<receiver
android:name=".widget.ShareRateWidgetProvider"
android:exported="true"
android:label="@string/widget_share_label">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/share_widget_info" />
</receiver>

<receiver
android:name=".worker.BootCompletedReceiver"
android:exported="false">
Expand Down
1 change: 0 additions & 1 deletion app/src/main/java/com/codeonyx/bcvaqui/BcvAquiApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ class BcvAquiApp : Application() {
super.onCreate()
createNotificationChannels()
WorkScheduler.scheduleAllBackgroundWork(this)
WorkScheduler.enqueueImmediateRefresh(this)
}

private fun createNotificationChannels() {
Expand Down
Loading
Loading