Skip to content

Commit c31fd1f

Browse files
committed
feat(v1.1): Backup cifrado AES-256, Historial de Accesos, LockScreen único
✨ Nuevas características: - Backup avanzado con cifrado AES-256-GCM y ShareSheet nativo - Historial de accesos con registro automático de desbloqueos - Reconstrucción de sugerencias con emojis automáticos 🐛 Correcciones: - Eliminada doble pantalla de biometría (consolidado en MainActivity) - Fixed race condition en bridge JavaScript-Kotlin 🔧 Cambios: - SecurityCheckActivity eliminada, MainActivity es ahora el launcher - Detección de duplicados insensible a mayúsculas en Set/Setting 📝 Añadido CHANGELOG.md y AccessHistoryManager.kt
1 parent 2817a32 commit c31fd1f

7 files changed

Lines changed: 426 additions & 34 deletions

File tree

CHANGELOG.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Changelog
2+
3+
Todos los cambios notables de este proyecto serán documentados en este archivo.
4+
5+
El formato está basado en [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/),
6+
y este proyecto adhiere a [Semantic Versioning](https://semver.org/lang/es/).
7+
8+
---
9+
10+
## [1.1] - 2026-01-13
11+
12+
### ✨ Añadido
13+
- **Backup Avanzado con Cifrado AES-256-GCM**
14+
- Modal de configuración con opciones de cifrado y contraseña
15+
- Inclusión opcional de multimedia (audios y fotos)
16+
- Derivación de clave segura con PBKDF2 (120.000 iteraciones)
17+
18+
- **ShareSheet Nativo**
19+
- Compartir backups directamente a Google Drive, Telegram, Email, etc.
20+
- Integración con `FileProvider` para acceso seguro a archivos
21+
22+
- **Restauración Inteligente**
23+
- Detección automática de backups cifrados
24+
- Solicitud de contraseña al importar archivos protegidos
25+
- Restauración completa de datos, audios y fotos
26+
27+
- **Pantalla de Perfil Nativa**
28+
- Nuevo UI para gestión de datos y backups
29+
- Accesible desde la web y desde el FAB de configuración
30+
31+
- **Historial de Accesos**
32+
- Registro automático de cada desbloqueo (biométrico/PIN)
33+
- Visualización de últimos 50 accesos con fecha y hora
34+
- Opción para limpiar historial
35+
36+
### 🐛 Corregido
37+
- Condición de carrera en el bridge JavaScript-Kotlin que impedía abrir la pantalla de perfil
38+
- Contexto de Compose no se resolvía correctamente para lanzar ShareSheet
39+
40+
### 🔧 Cambiado
41+
- **Reconstruir Sugerencias mejorado**
42+
- Ahora añade emojis automáticos basados en patrones de texto
43+
- Detección de duplicados insensible a mayúsculas
44+
- Muestra contador de elementos añadidos
45+
### 🔧 Cambiado
46+
- El botón "Backup Manual" en la web ahora abre la pantalla nativa de backup avanzado
47+
- Mejoras visuales en el diálogo de backup con descripción completa de contenidos
48+
49+
---
50+
51+
## [1.0] - 2025-12-XX
52+
53+
### ✨ Añadido
54+
- **Bitácora Psiconáutica Completa**
55+
- Registro de sustancias con nombre, color y emoji personalizable
56+
- Registro de entradas con dosis, unidad, fecha/hora, set, setting y notas
57+
58+
- **Notas de Voz**
59+
- Grabación y reproducción de notas de audio por entrada
60+
- Almacenamiento interno seguro
61+
62+
- **Fotos por Entrada**
63+
- Captura desde cámara o selección de galería
64+
- Visualización dentro de cada registro
65+
66+
- **Estadísticas y Gráficos**
67+
- Visualización de patrones de uso
68+
- Gráficos interactivos basados en Chart.js
69+
70+
- **Auto-Backup Periódico**
71+
- Backups automáticos cada 12 horas vía WorkManager
72+
- Rotación automática (máximo 7 backups)
73+
74+
- **Bloqueo de App**
75+
- PIN de acceso con auto-lock configurable
76+
- Pantalla de bloqueo con animación
77+
78+
- **Exportación CSV**
79+
- Exportar historial completo en formato CSV
80+
- Compatible con Excel, LibreOffice, etc.
81+
82+
- **Tema Suave**
83+
- Modo claro/oscuro alternativo con colores menos saturados
84+
85+
---
86+
87+
## Enlaces
88+
89+
- **Repositorio**: [github.com/D4vRAM369/PsychoLogger](https://github.com/D4vRAM369/PsychoLogger)
90+
- **Releases**: [github.com/D4vRAM369/PsychoLogger/releases](https://github.com/D4vRAM369/PsychoLogger/releases)

app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,26 +19,18 @@
1919
android:roundIcon="@mipmap/ic_launcher_round"
2020
android:supportsRtl="true"
2121
android:theme="@style/Theme.PsychoLogger">
22+
2223
<activity
23-
android:name=".SecurityCheckActivity"
24+
android:name=".MainActivity"
2425
android:exported="true"
2526
android:windowSoftInputMode="adjustResize|stateHidden"
2627
android:label="@string/app_name"
2728
android:theme="@style/Theme.PsychoLogger">
2829
<intent-filter>
2930
<action android:name="android.intent.action.MAIN" />
30-
3131
<category android:name="android.intent.category.LAUNCHER" />
3232
</intent-filter>
3333
</activity>
34-
35-
<activity
36-
android:name=".MainActivity"
37-
android:exported="false"
38-
android:windowSoftInputMode="adjustResize|stateHidden"
39-
android:label="@string/app_name"
40-
android:theme="@style/Theme.PsychoLogger">
41-
</activity>
4234

4335
<provider
4436
android:name="androidx.core.content.FileProvider"

app/src/main/assets/index.html

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8574,40 +8574,121 @@ <h3>📚 ${title}</h3>
85748574
/**
85758575
* RECONSTRUCCIÓN DE SUGERENCIAS (PBL: Reparación de Entorno)
85768576
* Escanea los registros existentes para recuperar sets/settings perdidos,
8577-
* ignorando textos corruptos (demasiado largos).
8577+
* añadiendo emojis automáticos y evitando duplicados.
85788578
*/
85798579
function rebuildSuggestionsFromEntries() {
8580-
console.log("Reconstruyendo sugerencias...");
8581-
if (!entries || !entries.length) return;
8580+
console.log("Reconstruyendo sugerencias con emojis...");
8581+
if (!entries || !entries.length) {
8582+
Android.showToast('❌ No hay registros para analizar');
8583+
return;
8584+
}
85828585

8586+
// Cargar opciones existentes
85838587
let customSets = JSON.parse(localStorage.getItem('psychologger_custom_sets') || '[]');
85848588
let customSettings = JSON.parse(localStorage.getItem('psychologger_custom_settings') || '[]');
85858589

8590+
// Normalizar: convertir strings antiguos a objetos {name, emoji}
8591+
customSets = customSets.map(item =>
8592+
typeof item === 'string' ? { name: item, emoji: suggestSetEmoji(item) } : item
8593+
);
8594+
customSettings = customSettings.map(item =>
8595+
typeof item === 'string' ? { name: item, emoji: suggestSettingEmoji(item) } : item
8596+
);
8597+
8598+
// Extraer nombres existentes para evitar duplicados
8599+
const existingSetNames = new Set(customSets.map(s => s.name.toLowerCase().trim()));
8600+
const existingSettingNames = new Set(customSettings.map(s => s.name.toLowerCase().trim()));
8601+
8602+
let addedSets = 0;
8603+
let addedSettings = 0;
8604+
85868605
entries.forEach(entry => {
8587-
// Validar SET: No vacío, no demasiado largo (protección), no duplicado
8588-
// Subimos a 40 caracteres para dar más margen
8606+
// Validar SET: No vacío, no muy largo, no duplicado
85898607
if (entry.set && entry.set.trim().length > 0 && entry.set.length <= 40) {
85908608
const trimmedSet = entry.set.trim();
8591-
if (!customSets.includes(trimmedSet)) customSets.push(trimmedSet);
8609+
const lowerSet = trimmedSet.toLowerCase();
8610+
if (!existingSetNames.has(lowerSet)) {
8611+
customSets.push({
8612+
name: trimmedSet,
8613+
emoji: suggestSetEmoji(trimmedSet)
8614+
});
8615+
existingSetNames.add(lowerSet);
8616+
addedSets++;
8617+
}
85928618
}
85938619

8594-
// Validar SETTING: No vacío, no demasiado largo, no duplicado
8620+
// Validar SETTING: No vacío, no muy largo, no duplicado
85958621
if (entry.setting && entry.setting.trim().length > 0 && entry.setting.length <= 40) {
85968622
const trimmedSetting = entry.setting.trim();
8597-
if (!customSettings.includes(trimmedSetting)) customSettings.push(trimmedSetting);
8623+
const lowerSetting = trimmedSetting.toLowerCase();
8624+
if (!existingSettingNames.has(lowerSetting)) {
8625+
customSettings.push({
8626+
name: trimmedSetting,
8627+
emoji: suggestSettingEmoji(trimmedSetting)
8628+
});
8629+
existingSettingNames.add(lowerSetting);
8630+
addedSettings++;
8631+
}
85988632
}
85998633
});
86008634

86018635
localStorage.setItem('psychologger_custom_sets', JSON.stringify(customSets));
86028636
localStorage.setItem('psychologger_custom_settings', JSON.stringify(customSettings));
86038637

8604-
// REFRESCAR UI: Esto es lo que faltaba para que aparezcan en los menús
8638+
// REFRESCAR UI
86058639
if (typeof loadCustomSetsAndSettings === 'function') {
86068640
loadCustomSetsAndSettings();
86078641
}
86088642

86098643
renderStats();
8610-
Android.showToast('✅ Sugerencias reconstruidas y menús actualizados');
8644+
Android.showToast(`✅ Reconstruido: +${addedSets} sets, +${addedSettings} settings`);
8645+
}
8646+
8647+
/**
8648+
* Sugiere un emoji apropiado para un SET (estado mental)
8649+
*/
8650+
function suggestSetEmoji(name) {
8651+
const lower = name.toLowerCase();
8652+
if (lower.includes('relaj') || lower.includes('calm')) return '😌';
8653+
if (lower.includes('ansi') || lower.includes('nervio')) return '😰';
8654+
if (lower.includes('feliz') || lower.includes('alegr')) return '😊';
8655+
if (lower.includes('triste') || lower.includes('melan')) return '😢';
8656+
if (lower.includes('medit') || lower.includes('zen')) return '🧘';
8657+
if (lower.includes('energ') || lower.includes('activ')) return '⚡';
8658+
if (lower.includes('cansad') || lower.includes('fatiga')) return '😴';
8659+
if (lower.includes('creativ') || lower.includes('inspir')) return '🎨';
8660+
if (lower.includes('social') || lower.includes('fiesta')) return '🎉';
8661+
if (lower.includes('introsp') || lower.includes('reflex')) return '🤔';
8662+
if (lower.includes('curios')) return '🔍';
8663+
if (lower.includes('paz') || lower.includes('sereno')) return '☮️';
8664+
if (lower.includes('amor') || lower.includes('carinˇo')) return '❤️';
8665+
if (lower.includes('miedo') || lower.includes('temor')) return '😨';
8666+
if (lower.includes('eufori') || lower.includes('excita')) return '🤩';
8667+
return '🧠'; // Default para SET
8668+
}
8669+
8670+
/**
8671+
* Sugiere un emoji apropiado para un SETTING (entorno físico)
8672+
*/
8673+
function suggestSettingEmoji(name) {
8674+
const lower = name.toLowerCase();
8675+
if (lower.includes('casa') || lower.includes('hogar')) return '🏠';
8676+
if (lower.includes('playa') || lower.includes('mar')) return '🏖️';
8677+
if (lower.includes('montaña') || lower.includes('campo')) return '🏔️';
8678+
if (lower.includes('bosque') || lower.includes('naturaleza')) return '🌲';
8679+
if (lower.includes('parque') || lower.includes('jardin')) return '🌳';
8680+
if (lower.includes('club') || lower.includes('disco') || lower.includes('fiesta')) return '🎶';
8681+
if (lower.includes('concierto') || lower.includes('festival')) return '🎪';
8682+
if (lower.includes('amigo') || lower.includes('grupo')) return '👥';
8683+
if (lower.includes('solo') || lower.includes('solitario')) return '🧍';
8684+
if (lower.includes('noche') || lower.includes('oscur')) return '🌙';
8685+
if (lower.includes('dia') || lower.includes('sol')) return '☀️';
8686+
if (lower.includes('habitacion') || lower.includes('cuarto')) return '🛏️';
8687+
if (lower.includes('ciudad') || lower.includes('urban')) return '🏙️';
8688+
if (lower.includes('viaje') || lower.includes('road')) return '🚗';
8689+
if (lower.includes('trabajo') || lower.includes('oficina')) return '💼';
8690+
if (lower.includes('terapia') || lower.includes('clinica')) return '🏥';
8691+
return '📍'; // Default para SETTING
86118692
}
86128693

86138694
</script>
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package com.d4vram.psychologger
2+
3+
import android.content.Context
4+
import android.content.SharedPreferences
5+
import org.json.JSONArray
6+
import org.json.JSONObject
7+
import java.text.SimpleDateFormat
8+
import java.util.*
9+
10+
/**
11+
* AccessHistoryManager - Gestiona el historial de accesos a la app
12+
*
13+
* Almacena los últimos MAX_EVENTS eventos de desbloqueo con:
14+
* - Timestamp del acceso
15+
* - Método utilizado (biometric, pin, init)
16+
*/
17+
class AccessHistoryManager(context: Context) {
18+
19+
companion object {
20+
private const val PREFS_NAME = "access_history_prefs"
21+
private const val KEY_HISTORY = "access_history"
22+
private const val MAX_EVENTS = 50
23+
}
24+
25+
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
26+
27+
/**
28+
* Representa un evento de acceso
29+
*/
30+
data class AccessEvent(
31+
val timestamp: Long,
32+
val method: String // "biometric" | "pin" | "init" | "device_credential"
33+
) {
34+
val formattedDate: String
35+
get() = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()).format(Date(timestamp))
36+
37+
val formattedTime: String
38+
get() = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date(timestamp))
39+
40+
val methodLabel: String
41+
get() = when (method) {
42+
"biometric" -> "🔐 Biometría"
43+
"pin" -> "🔢 PIN"
44+
"device_credential" -> "🔑 Credencial del dispositivo"
45+
"init" -> "🚀 Inicio"
46+
else -> "❓ Desconocido"
47+
}
48+
49+
fun toJson(): JSONObject = JSONObject().apply {
50+
put("timestamp", timestamp)
51+
put("method", method)
52+
}
53+
54+
companion object {
55+
fun fromJson(json: JSONObject): AccessEvent {
56+
return AccessEvent(
57+
timestamp = json.getLong("timestamp"),
58+
method = json.getString("method")
59+
)
60+
}
61+
}
62+
}
63+
64+
/**
65+
* Registra un nuevo acceso
66+
*
67+
* @param method Método de autenticación utilizado
68+
*/
69+
fun logAccess(method: String) {
70+
val history = getHistoryMutable()
71+
72+
// Añadir nuevo evento al principio
73+
history.add(0, AccessEvent(
74+
timestamp = System.currentTimeMillis(),
75+
method = method
76+
))
77+
78+
// Limitar a MAX_EVENTS
79+
while (history.size > MAX_EVENTS) {
80+
history.removeAt(history.size - 1)
81+
}
82+
83+
// Persistir
84+
saveHistory(history)
85+
}
86+
87+
/**
88+
* Obtiene el historial de accesos (más recientes primero)
89+
*/
90+
fun getHistory(): List<AccessEvent> {
91+
return getHistoryMutable().toList()
92+
}
93+
94+
/**
95+
* Limpia todo el historial
96+
*/
97+
fun clearHistory() {
98+
prefs.edit().remove(KEY_HISTORY).apply()
99+
}
100+
101+
/**
102+
* Obtiene el número de accesos registrados
103+
*/
104+
fun getAccessCount(): Int {
105+
return getHistory().size
106+
}
107+
108+
private fun getHistoryMutable(): MutableList<AccessEvent> {
109+
val jsonString = prefs.getString(KEY_HISTORY, null) ?: return mutableListOf()
110+
111+
return try {
112+
val jsonArray = JSONArray(jsonString)
113+
val list = mutableListOf<AccessEvent>()
114+
for (i in 0 until jsonArray.length()) {
115+
list.add(AccessEvent.fromJson(jsonArray.getJSONObject(i)))
116+
}
117+
list
118+
} catch (e: Exception) {
119+
mutableListOf()
120+
}
121+
}
122+
123+
private fun saveHistory(history: List<AccessEvent>) {
124+
val jsonArray = JSONArray()
125+
history.forEach { event ->
126+
jsonArray.put(event.toJson())
127+
}
128+
prefs.edit().putString(KEY_HISTORY, jsonArray.toString()).apply()
129+
}
130+
}

0 commit comments

Comments
 (0)