Compare commits
7 Commits
8b8ff223fb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 833b3678bc | |||
| 0161858148 | |||
| 68fac80520 | |||
| aa02d7c896 | |||
| ec57ac366d | |||
| e0133d2ca2 | |||
| f2fd1c3bf5 |
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
.venv/
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
.claude/
|
||||
*.iml
|
||||
|
||||
# Archivos temporales
|
||||
*.log
|
||||
*.tmp
|
||||
.cache/
|
||||
288
README.md
288
README.md
@@ -1,2 +1,288 @@
|
||||
# adif-api-reverse-enginereeng
|
||||
# ADIF API - Reverse Engineering ✅
|
||||
|
||||
Cliente Python completo para acceder a la API de ADIF (El Cano Móvil) mediante ingeniería reversa.
|
||||
|
||||
> **Estado del Proyecto**: ✅ **COMPLETADO CON ÉXITO**
|
||||
> Autenticación HMAC-SHA256 implementada, 4/8 endpoints funcionales, 1587 códigos de estación extraídos.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Inicio Rápido
|
||||
|
||||
```bash
|
||||
# Instalar dependencias
|
||||
pip install requests
|
||||
|
||||
# Ejecutar demo
|
||||
python3 adif_client.py
|
||||
```
|
||||
|
||||
### Uso Básico
|
||||
|
||||
```python
|
||||
from adif_client import AdifClient
|
||||
|
||||
# Inicializar cliente
|
||||
client = AdifClient(
|
||||
access_key="and20210615",
|
||||
secret_key="Jthjtr946RTt"
|
||||
)
|
||||
|
||||
# Obtener salidas de Madrid Atocha
|
||||
trains = client.get_departures("10200", "AVLDMD")
|
||||
|
||||
for train in trains:
|
||||
info = train['commercialPathInfo']
|
||||
print(f"Tren {info['commercialPathKey']['commercialCirculationKey']['commercialNumber']}")
|
||||
|
||||
# Obtener ruta completa de un tren
|
||||
route = client.get_train_route(
|
||||
commercial_number="03194",
|
||||
launching_date=1764889200000,
|
||||
origin_station_code="10200",
|
||||
destination_station_code="71801"
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Estado del Proyecto
|
||||
|
||||
### ✅ Funcionalidades Implementadas
|
||||
|
||||
| Característica | Estado | Descripción |
|
||||
|----------------|--------|-------------|
|
||||
| Extracción de claves | ✅ | Claves extraídas de `libapi-keys.so` con Ghidra |
|
||||
| Algoritmo HMAC-SHA256 | ✅ | Implementación completa y validada |
|
||||
| Códigos de estación | ✅ | 1587 estaciones extraídas |
|
||||
| Endpoints funcionales | ✅ | 4/8 endpoints (50%) |
|
||||
| Cliente Python | ✅ | API completa y lista para usar |
|
||||
| Documentación | ✅ | Completa en `/docs` |
|
||||
|
||||
### 📍 Endpoints Disponibles
|
||||
|
||||
#### ✅ Funcionales (4/8)
|
||||
|
||||
| Método | Endpoint | Descripción |
|
||||
|--------|----------|-------------|
|
||||
| `get_departures()` | `/departures/traffictype/` | Salidas de una estación |
|
||||
| `get_arrivals()` | `/arrivals/traffictype/` | Llegadas a una estación |
|
||||
| `get_train_route()` | `/onepaths/` | Ruta completa de un tren |
|
||||
| `get_station_observations()` | `/stationsobservations/` | Observaciones de estaciones |
|
||||
|
||||
#### ❌ Bloqueados por Permisos (4/8)
|
||||
|
||||
- `/betweenstations/traffictype/` - 401 Unauthorized
|
||||
- `/onestation/` - 401 Unauthorized
|
||||
- `/severalpaths/` - 401 Unauthorized
|
||||
- `/compositions/path/` - 401 Unauthorized
|
||||
|
||||
**Nota**: Los endpoints bloqueados tienen implementación correcta pero las claves no tienen permisos suficientes.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Estructura del Proyecto
|
||||
|
||||
```
|
||||
adif-api-reverse-engineering/
|
||||
├── 📄 README.md # Este archivo
|
||||
├── 📄 LICENSE # Licencia MIT
|
||||
│
|
||||
├── 🐍 Python Scripts (Core)
|
||||
│ ├── adif_auth.py # ⭐ Implementación HMAC-SHA256
|
||||
│ ├── adif_client.py # ⭐ Cliente completo de la API
|
||||
│ ├── query_api.py # CLI interactivo
|
||||
│ └── generate_curl.py # Generador de curls
|
||||
│
|
||||
├── 📊 Datos
|
||||
│ ├── station_codes.txt # ⭐ 1587 códigos de estación
|
||||
│ └── extracted_keys.txt # Claves extraídas
|
||||
│
|
||||
├── 🧪 Tests
|
||||
│ ├── test_endpoints_detailed.py # Test exhaustivo con debug
|
||||
│ └── test_onepaths_with_real_trains.py # Test con datos reales
|
||||
│
|
||||
├── 📚 Documentación (/docs)
|
||||
│ ├── FINAL_STATUS_REPORT.md # Informe completo
|
||||
│ ├── API_DOCUMENTATION.md # Documentación de API
|
||||
│ ├── AUTHENTICATION_ALGORITHM.md # Algoritmo HMAC
|
||||
│ ├── ENDPOINTS_ANALYSIS.md # Análisis de endpoints
|
||||
│ ├── API_REQUEST_BODIES.md # Payloads documentados
|
||||
│ ├── GHIDRA_GUIDE.md # Tutorial de Ghidra
|
||||
│ ├── NEW_DISCOVERIES.md # Últimos descubrimientos
|
||||
│ └── CLAUDE.md # Contexto del proyecto
|
||||
│
|
||||
├── 📦 APK & Análisis
|
||||
│ ├── base.apk # APK original
|
||||
│ ├── apk_decompiled/ # Código decompilado (JADX)
|
||||
│ ├── apk_extracted/ # APK extraído
|
||||
│ │ ├── assets/stations_all.json # Fuente de estaciones
|
||||
│ │ └── lib/x86_64/libapi-keys.so # Librería con claves
|
||||
│ └── frida_scripts/ # Scripts de análisis dinámico
|
||||
│
|
||||
└── 🗂️ Otros
|
||||
├── archived_tests/ # Tests antiguos archivados
|
||||
└── api_testing_scripts/ # Scripts auxiliares
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Autenticación
|
||||
|
||||
### Claves Extraídas
|
||||
|
||||
```python
|
||||
ACCESS_KEY = "and20210615"
|
||||
SECRET_KEY = "Jthjtr946RTt"
|
||||
USER_KEY_CIRCULATION = "f4ce9fbfa9d721e39b8984805901b5df"
|
||||
USER_KEY_STATIONS = "0d021447a2fd2ac64553674d5a0c1a6f"
|
||||
```
|
||||
|
||||
**Fuente**: `apk_extracted/lib/x86_64/libapi-keys.so` (Ghidra)
|
||||
|
||||
### Algoritmo HMAC-SHA256
|
||||
|
||||
Implementación basada en AWS Signature v4:
|
||||
|
||||
**⚠️ CRÍTICO**: El orden de headers NO es alfabético:
|
||||
|
||||
```python
|
||||
canonical_headers = (
|
||||
f"content-type:application/json\n"
|
||||
f"x-elcano-host:{host}\n" # ← NO alfabético
|
||||
f"x-elcano-client:api-elcano\n"
|
||||
f"x-elcano-date:{timestamp}\n"
|
||||
f"x-elcano-userid:{user_id}\n"
|
||||
)
|
||||
```
|
||||
|
||||
Ver `adif_auth.py` para implementación completa.
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ Códigos de Estación
|
||||
|
||||
**Total**: 1587 estaciones
|
||||
**Archivo**: `station_codes.txt`
|
||||
**Formato**: `código TAB nombre TAB tipos_tráfico`
|
||||
|
||||
### Top 10 Estaciones
|
||||
|
||||
```
|
||||
10200 Madrid Puerta de Atocha AVLDMD
|
||||
10302 Madrid Chamartín-Clara Campoamor AVLDMD
|
||||
71801 Barcelona Sants AVLDMD,CERCANIAS
|
||||
60000 València Nord AVLDMD
|
||||
11401 Sevilla Santa Justa AVLDMD
|
||||
50003 Alacant Terminal AVLDMD,CERCANIAS
|
||||
54007 Córdoba Central AVLDMD
|
||||
79600 Zaragoza Portillo AVLDMD,CERCANIAS
|
||||
03216 València J.Sorolla AVLDMD
|
||||
04040 Zaragoza Delicias AVLDMD,CERCANIAS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Casos de Uso
|
||||
|
||||
### 1. Monitor de Retrasos
|
||||
|
||||
```python
|
||||
import time
|
||||
from adif_client import AdifClient
|
||||
|
||||
client = AdifClient(ACCESS_KEY, SECRET_KEY)
|
||||
|
||||
while True:
|
||||
trains = client.get_departures("10200", "ALL")
|
||||
for train in trains:
|
||||
passthrough = train.get('passthroughStep', {})
|
||||
dep_sides = passthrough.get('departurePassthroughStepSides', {})
|
||||
delay = dep_sides.get('forecastedOrAuditedDelay', 0)
|
||||
|
||||
if delay > 300: # Más de 5 minutos
|
||||
print(f"⚠️ Retraso de {delay//60} min")
|
||||
|
||||
time.sleep(30)
|
||||
```
|
||||
|
||||
### 2. Consultar Rutas Completas
|
||||
|
||||
```python
|
||||
# Obtener trenes con sus rutas
|
||||
trains_with_routes = client.get_all_departures_with_routes(
|
||||
station_code="10200",
|
||||
traffic_type="AVLDMD",
|
||||
max_trains=5
|
||||
)
|
||||
|
||||
for train in trains_with_routes:
|
||||
print(f"🚄 Tren {train['commercial_number']}")
|
||||
print(f" Paradas: {len(train['route'])}")
|
||||
```
|
||||
|
||||
### 3. CLI Interactivo
|
||||
|
||||
```bash
|
||||
python3 query_api.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔬 Herramientas Utilizadas
|
||||
|
||||
- **Ghidra** - Extracción de claves de `libapi-keys.so`
|
||||
- **JADX** - Decompilación del APK
|
||||
- **Python 3** - Implementación del cliente
|
||||
- **Frida** (opcional) - Análisis dinámico
|
||||
|
||||
---
|
||||
|
||||
## 📖 Documentación
|
||||
|
||||
Toda la documentación está en `/docs`:
|
||||
|
||||
- **[FINAL_STATUS_REPORT.md](docs/FINAL_STATUS_REPORT.md)** - Informe completo del proyecto
|
||||
- **[API_DOCUMENTATION.md](docs/API_DOCUMENTATION.md)** - Documentación de la API
|
||||
- **[AUTHENTICATION_ALGORITHM.md](docs/AUTHENTICATION_ALGORITHM.md)** - Algoritmo HMAC detallado
|
||||
- **[GHIDRA_GUIDE.md](docs/GHIDRA_GUIDE.md)** - Tutorial paso a paso
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Logros del Proyecto
|
||||
|
||||
✅ Claves de autenticación extraídas con Ghidra
|
||||
✅ Algoritmo HMAC-SHA256 implementado y validado
|
||||
✅ 1587 códigos de estación disponibles
|
||||
✅ 4/8 endpoints funcionales (50%)
|
||||
✅ Cliente Python listo para producción
|
||||
✅ Documentación completa
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Limitaciones
|
||||
|
||||
- 4/8 endpoints bloqueados por permisos del servidor
|
||||
- Las claves extraídas son de perfil "anónimo/básico"
|
||||
- No hay acceso a información de usuario autenticado
|
||||
|
||||
---
|
||||
|
||||
## 📄 Licencia
|
||||
|
||||
MIT License - Ver [LICENSE](LICENSE)
|
||||
|
||||
⚠️ **Disclaimer**: Proyecto con fines educativos y de investigación. Úsalo de forma responsable.
|
||||
|
||||
---
|
||||
|
||||
## ✨ Créditos
|
||||
|
||||
- **ADIF** - Por la aplicación El Cano Móvil
|
||||
- **Ghidra** & **JADX** - Herramientas de reverse engineering
|
||||
- **Comunidad de seguridad** - Por compartir conocimiento
|
||||
|
||||
---
|
||||
|
||||
**Última actualización**: 2025-12-05
|
||||
**Estado**: ✅ Proyecto completado con éxito
|
||||
|
||||
459
adif_auth.py
Executable file
459
adif_auth.py
Executable file
@@ -0,0 +1,459 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ADIF API Authenticator - Réplica del Sistema Original
|
||||
|
||||
Este módulo es una réplica fiel del algoritmo de autenticación HMAC-SHA256
|
||||
utilizado por la API de ADIF (El Cano Móvil), obtenido mediante ingeniería
|
||||
reversa del código fuente original en ElcanoAuth.java.
|
||||
|
||||
El algoritmo sigue el patrón AWS Signature Version 4 con características
|
||||
específicas de ADIF:
|
||||
- Derivación de claves en cascada (date_key -> client_key -> signature_key)
|
||||
- Orden NO alfabético de headers canónicos (crítico para el funcionamiento)
|
||||
- Timestamp en formato ISO 8601 con zona horaria UTC
|
||||
|
||||
Fuente Original:
|
||||
apk_decompiled/sources/com/adif/elcanomovil/serviceNetworking/interceptors/auth/ElcanoAuth.java
|
||||
|
||||
Uso:
|
||||
auth = AdifAuthenticator(access_key="YOUR_KEY", secret_key="YOUR_KEY")
|
||||
headers = auth.get_auth_headers("POST", url, payload={...})
|
||||
response = requests.post(url, json=payload, headers=headers)
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
from datetime import datetime
|
||||
import json
|
||||
import uuid
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
class AdifAuthenticator:
|
||||
"""
|
||||
Implementa el algoritmo de autenticación HMAC-SHA256 de ADIF
|
||||
Similar a AWS Signature Version 4
|
||||
"""
|
||||
|
||||
# User-keys estáticas (diferentes de las claves HMAC)
|
||||
USER_KEY_CIRCULATION = "f4ce9fbfa9d721e39b8984805901b5df"
|
||||
USER_KEY_STATIONS = "0d021447a2fd2ac64553674d5a0c1a6f"
|
||||
|
||||
def __init__(self, access_key, secret_key):
|
||||
"""
|
||||
Inicializa el autenticador con las claves HMAC
|
||||
|
||||
Args:
|
||||
access_key (str): Access key extraída de libapi-keys.so
|
||||
secret_key (str): Secret key extraída de libapi-keys.so
|
||||
"""
|
||||
self.access_key = access_key
|
||||
self.secret_key = secret_key
|
||||
|
||||
def get_timestamp(self, date=None):
|
||||
"""
|
||||
Genera timestamp en formato ISO 8601 compacto UTC
|
||||
|
||||
Args:
|
||||
date (datetime): Fecha a formatear (por defecto: ahora)
|
||||
|
||||
Returns:
|
||||
str: Timestamp en formato yyyyMMddTHHmmssZ
|
||||
|
||||
Ejemplo:
|
||||
"20251204T204637Z"
|
||||
"""
|
||||
if date is None:
|
||||
date = datetime.utcnow()
|
||||
return date.strftime('%Y%m%dT%H%M%SZ')
|
||||
|
||||
def get_date(self, date=None):
|
||||
"""
|
||||
Genera fecha en formato compacto
|
||||
|
||||
Args:
|
||||
date (datetime): Fecha a formatear (por defecto: ahora)
|
||||
|
||||
Returns:
|
||||
str: Fecha en formato yyyyMMdd
|
||||
|
||||
Ejemplo:
|
||||
"20251204"
|
||||
"""
|
||||
if date is None:
|
||||
date = datetime.utcnow()
|
||||
return date.strftime('%Y%m%d')
|
||||
|
||||
def format_payload(self, payload):
|
||||
"""
|
||||
Formatea el payload JSON eliminando espacios y saltos de línea
|
||||
(ElcanoAuth.java:86-91)
|
||||
|
||||
Args:
|
||||
payload: Diccionario o string con el payload
|
||||
|
||||
Returns:
|
||||
str: Payload formateado sin espacios
|
||||
|
||||
Ejemplo:
|
||||
Input: {"page": {"pageNumber": 0}}
|
||||
Output: {"page":{"pageNumber":0}}
|
||||
"""
|
||||
if payload is None:
|
||||
return ""
|
||||
|
||||
if isinstance(payload, dict):
|
||||
payload = json.dumps(payload, separators=(',', ':'))
|
||||
|
||||
return payload.replace('\r', '').replace('\n', '').replace(' ', '')
|
||||
|
||||
def sha256_hash(self, text):
|
||||
"""
|
||||
Calcula SHA-256 hash en formato hexadecimal
|
||||
(ElcanoAuth.java:185-193)
|
||||
|
||||
Args:
|
||||
text (str): Texto a hashear
|
||||
|
||||
Returns:
|
||||
str: Hash SHA-256 en hexadecimal (64 caracteres)
|
||||
"""
|
||||
return hashlib.sha256(text.encode('utf-8')).hexdigest()
|
||||
|
||||
def hmac_sha256(self, key, data):
|
||||
"""
|
||||
Calcula HMAC-SHA256
|
||||
(ElcanoAuth.java:117-127)
|
||||
|
||||
Args:
|
||||
key: Clave (str o bytes)
|
||||
data (str): Datos a firmar
|
||||
|
||||
Returns:
|
||||
bytes: Firma HMAC-SHA256 (32 bytes)
|
||||
"""
|
||||
if isinstance(key, str):
|
||||
key = key.encode('utf-8')
|
||||
|
||||
return hmac.new(key, data.encode('utf-8'), hashlib.sha256).digest()
|
||||
|
||||
def get_signature_key(self, date_simple, client):
|
||||
"""
|
||||
Genera la clave de firma mediante derivación en cascada
|
||||
(ElcanoAuth.java:109-111)
|
||||
|
||||
Proceso:
|
||||
kDate = HMAC(secretKey, date)
|
||||
kClient = HMAC(kDate, client)
|
||||
kSigning = HMAC(kClient, "elcano_request")
|
||||
|
||||
Args:
|
||||
date_simple (str): Fecha en formato yyyyMMdd
|
||||
client (str): Nombre del cliente (ej: "AndroidElcanoApp")
|
||||
|
||||
Returns:
|
||||
bytes: Clave de firma derivada (32 bytes)
|
||||
"""
|
||||
k_date = self.hmac_sha256(self.secret_key, date_simple)
|
||||
k_client = self.hmac_sha256(k_date, client)
|
||||
k_signing = self.hmac_sha256(k_client, "elcano_request")
|
||||
|
||||
return k_signing
|
||||
|
||||
def prepare_canonical_request(self, method, path, params, payload,
|
||||
content_type, host, client, timestamp, user_id):
|
||||
"""
|
||||
Prepara la petición canónica para firma
|
||||
(ElcanoAuth.java:129-172)
|
||||
|
||||
Estructura:
|
||||
<HTTPMethod>
|
||||
<Path>
|
||||
<QueryString>
|
||||
content-type:<ContentType>
|
||||
x-elcano-client:<Client>
|
||||
x-elcano-date:<Timestamp>
|
||||
x-elcano-host:<Host>
|
||||
x-elcano-userid:<UserId>
|
||||
content-type;x-elcano-client;x-elcano-date;x-elcano-host;x-elcano-userid
|
||||
<SHA256HashOfPayload>
|
||||
|
||||
Args:
|
||||
method (str): Método HTTP (GET, POST, etc.)
|
||||
path (str): Path de la URL
|
||||
params (str): Query string (puede ser vacío)
|
||||
payload: Body de la petición
|
||||
content_type (str): Content-Type
|
||||
host (str): Host del servidor
|
||||
client (str): Nombre del cliente
|
||||
timestamp (str): Timestamp de la petición
|
||||
user_id (str): UUID del usuario
|
||||
|
||||
Returns:
|
||||
tuple: (canonical_request, signed_headers)
|
||||
"""
|
||||
# Formatear payload
|
||||
formatted_payload = self.format_payload(payload)
|
||||
payload_hash = self.sha256_hash(formatted_payload)
|
||||
|
||||
# Headers canónicos (ORDEN ESPECÍFICO, no alfabético completo!)
|
||||
# Nota: El orden DEBE coincidir exactamente con ElcanoAuth.java:137-165
|
||||
canonical_headers = (
|
||||
f"content-type:{content_type}\n"
|
||||
f"x-elcano-host:{host}\n" # ← Segundo (antes de client!)
|
||||
f"x-elcano-client:{client}\n" # ← Tercero
|
||||
f"x-elcano-date:{timestamp}\n" # ← Cuarto
|
||||
f"x-elcano-userid:{user_id}\n" # ← Quinto
|
||||
)
|
||||
|
||||
# Lista de headers firmados (MISMO orden que canonical_headers)
|
||||
signed_headers = "content-type;x-elcano-host;x-elcano-client;x-elcano-date;x-elcano-userid"
|
||||
|
||||
# Construir canonical request
|
||||
canonical_request = (
|
||||
f"{method}\n"
|
||||
f"{path}\n"
|
||||
f"{params}\n"
|
||||
f"{canonical_headers}"
|
||||
f"{signed_headers}\n"
|
||||
f"{payload_hash}"
|
||||
)
|
||||
|
||||
return canonical_request, signed_headers
|
||||
|
||||
def prepare_string_to_sign(self, timestamp, date_simple, client, user_id, canonical_request):
|
||||
"""
|
||||
Prepara el string a firmar
|
||||
(ElcanoAuth.java:174-183)
|
||||
|
||||
Estructura:
|
||||
HMAC-SHA256
|
||||
<Timestamp>
|
||||
<Date>/<Client>/<UserId>/elcano_request
|
||||
<SHA256HashOfCanonicalRequest>
|
||||
|
||||
Args:
|
||||
timestamp (str): Timestamp ISO compacto
|
||||
date_simple (str): Fecha simple (yyyyMMdd)
|
||||
client (str): Nombre del cliente
|
||||
user_id (str): UUID del usuario
|
||||
canonical_request (str): Petición canónica
|
||||
|
||||
Returns:
|
||||
str: String to sign
|
||||
"""
|
||||
canonical_hash = self.sha256_hash(canonical_request)
|
||||
|
||||
string_to_sign = (
|
||||
f"HMAC-SHA256\n"
|
||||
f"{timestamp}\n"
|
||||
f"{date_simple}/{client}/{user_id}/elcano_request\n"
|
||||
f"{canonical_hash}"
|
||||
)
|
||||
|
||||
return string_to_sign
|
||||
|
||||
def calculate_signature(self, string_to_sign, date_simple, client):
|
||||
"""
|
||||
Calcula la firma final
|
||||
(ElcanoAuth.java:78-84)
|
||||
|
||||
Args:
|
||||
string_to_sign (str): String preparado para firma
|
||||
date_simple (str): Fecha simple
|
||||
client (str): Nombre del cliente
|
||||
|
||||
Returns:
|
||||
str: Firma en hexadecimal
|
||||
"""
|
||||
signing_key = self.get_signature_key(date_simple, client)
|
||||
signature_bytes = hmac.new(signing_key, string_to_sign.encode('utf-8'), hashlib.sha256).digest()
|
||||
|
||||
# Convertir a hexadecimal (minúsculas)
|
||||
signature = signature_bytes.hex()
|
||||
|
||||
return signature
|
||||
|
||||
def build_authorization_header(self, signature, date_simple, client, user_id, signed_headers):
|
||||
"""
|
||||
Construye el header Authorization
|
||||
(ElcanoAuth.java:61-63)
|
||||
|
||||
Formato:
|
||||
HMAC-SHA256 Credential=<accessKey>/<date>/<client>/<userId>/elcano_request,
|
||||
SignedHeaders=<headers>,Signature=<signature>
|
||||
|
||||
Args:
|
||||
signature (str): Firma calculada
|
||||
date_simple (str): Fecha simple
|
||||
client (str): Nombre del cliente
|
||||
user_id (str): UUID del usuario
|
||||
signed_headers (str): Lista de headers firmados
|
||||
|
||||
Returns:
|
||||
str: Header Authorization completo
|
||||
"""
|
||||
return (
|
||||
f"HMAC-SHA256 "
|
||||
f"Credential={self.access_key}/{date_simple}/{client}/{user_id}/elcano_request,"
|
||||
f"SignedHeaders={signed_headers},"
|
||||
f"Signature={signature}"
|
||||
)
|
||||
|
||||
def get_auth_headers(self, method, url, payload=None, user_id=None, date=None):
|
||||
"""
|
||||
Genera todos los headers necesarios para autenticación
|
||||
|
||||
Args:
|
||||
method (str): Método HTTP (GET, POST, etc.)
|
||||
url (str): URL completa de la petición
|
||||
payload: Body de la petición (dict o None)
|
||||
user_id (str): UUID del usuario (se genera si no se provee)
|
||||
date (datetime): Fecha de la petición (por defecto: ahora)
|
||||
|
||||
Returns:
|
||||
dict: Headers completos para la petición
|
||||
|
||||
Ejemplo:
|
||||
>>> auth = AdifAuthenticator(access_key="...", secret_key="...")
|
||||
>>> headers = auth.get_auth_headers(
|
||||
... "POST",
|
||||
... "https://circulacion.api.adif.es/path",
|
||||
... payload={"page": {"pageNumber": 0}}
|
||||
... )
|
||||
>>> headers
|
||||
{
|
||||
"Content-Type": "application/json;charset=utf-8",
|
||||
"X-Elcano-Host": "circulacion.api.adif.es",
|
||||
"X-Elcano-Client": "AndroidElcanoApp",
|
||||
"X-Elcano-Date": "20251204T204637Z",
|
||||
"X-Elcano-UserId": "a1b2c3d4-...",
|
||||
"Authorization": "HMAC-SHA256 Credential=..."
|
||||
}
|
||||
"""
|
||||
# Parse URL
|
||||
parsed = urlparse(url)
|
||||
host = parsed.netloc
|
||||
path = parsed.path
|
||||
params = parsed.query or ""
|
||||
|
||||
# Defaults
|
||||
if user_id is None:
|
||||
user_id = str(uuid.uuid4())
|
||||
|
||||
if date is None:
|
||||
date = datetime.utcnow()
|
||||
|
||||
client = "AndroidElcanoApp"
|
||||
content_type = "application/json;charset=utf-8"
|
||||
|
||||
# Generar timestamps
|
||||
timestamp = self.get_timestamp(date)
|
||||
date_simple = self.get_date(date)
|
||||
|
||||
# 1. Preparar canonical request
|
||||
canonical_request, signed_headers = self.prepare_canonical_request(
|
||||
method, path, params, payload, content_type, host, client, timestamp, user_id
|
||||
)
|
||||
|
||||
# 2. Preparar string to sign
|
||||
string_to_sign = self.prepare_string_to_sign(
|
||||
timestamp, date_simple, client, user_id, canonical_request
|
||||
)
|
||||
|
||||
# 3. Calcular firma
|
||||
signature = self.calculate_signature(string_to_sign, date_simple, client)
|
||||
|
||||
# 4. Construir header Authorization
|
||||
authorization = self.build_authorization_header(
|
||||
signature, date_simple, client, user_id, signed_headers
|
||||
)
|
||||
|
||||
# 5. Retornar todos los headers
|
||||
return {
|
||||
"Content-Type": content_type,
|
||||
"X-Elcano-Host": host,
|
||||
"X-Elcano-Client": client,
|
||||
"X-Elcano-Date": timestamp,
|
||||
"X-Elcano-UserId": user_id,
|
||||
"Authorization": authorization
|
||||
}
|
||||
|
||||
def get_user_key_for_url(self, url):
|
||||
"""
|
||||
Obtiene la User-key estática correcta según la URL
|
||||
|
||||
Args:
|
||||
url (str): URL de la petición
|
||||
|
||||
Returns:
|
||||
str: User-key correspondiente
|
||||
"""
|
||||
if "circulacion.api.adif.es" in url:
|
||||
return self.USER_KEY_CIRCULATION
|
||||
elif "estaciones.api.adif.es" in url:
|
||||
return self.USER_KEY_STATIONS
|
||||
else:
|
||||
return self.USER_KEY_CIRCULATION # Por defecto
|
||||
|
||||
|
||||
def example_usage():
|
||||
"""
|
||||
Ejemplo de uso del autenticador
|
||||
"""
|
||||
print("="*70)
|
||||
print("ADIF API Authenticator - Ejemplo de Uso")
|
||||
print("="*70)
|
||||
|
||||
# PASO 1: Obtener las claves de libapi-keys.so
|
||||
# (Usar Ghidra o Frida para extraerlas)
|
||||
print("\n⚠️ IMPORTANTE: Reemplazar con las claves reales extraídas de libapi-keys.so")
|
||||
print(" Ver AUTHENTICATION_ALGORITHM.md para instrucciones de extracción\n")
|
||||
|
||||
ACCESS_KEY = "and20210615" # ✅ Extraído con Ghidra
|
||||
SECRET_KEY = "Jthjtr946RTt" # ✅ Extraído con Ghidra
|
||||
|
||||
# PASO 2: Crear el autenticador
|
||||
auth = AdifAuthenticator(access_key=ACCESS_KEY, secret_key=SECRET_KEY)
|
||||
|
||||
# PASO 3: Preparar la petición
|
||||
url = "https://circulacion.api.adif.es/portroyalmanager/secure/circulationpaths/departures/traffictype/"
|
||||
payload = {
|
||||
"commercialService": "BOTH",
|
||||
"commercialStopType": "BOTH",
|
||||
"page": {"pageNumber": 0},
|
||||
"stationCode": "10200", # Madrid Atocha
|
||||
"trafficType": "ALL"
|
||||
}
|
||||
|
||||
# PASO 4: Generar headers de autenticación
|
||||
headers = auth.get_auth_headers("POST", url, payload=payload)
|
||||
|
||||
# PASO 5: Añadir User-key estática
|
||||
headers["User-key"] = auth.get_user_key_for_url(url)
|
||||
|
||||
# PASO 6: Mostrar resultado
|
||||
print("Headers generados:")
|
||||
print("-" * 70)
|
||||
for key, value in headers.items():
|
||||
print(f"{key}: {value}")
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("Para hacer la petición:")
|
||||
print("="*70)
|
||||
print("""
|
||||
import requests
|
||||
|
||||
response = requests.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
print(f"Status: {response.status_code}")
|
||||
print(response.json())
|
||||
""")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
example_usage()
|
||||
392
adif_client.py
Executable file
392
adif_client.py
Executable file
@@ -0,0 +1,392 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Cliente completo de la API de ADIF
|
||||
|
||||
Implementa todos los endpoints funcionales con métodos simples de usar.
|
||||
Incluye manejo de errores y validación de datos.
|
||||
"""
|
||||
|
||||
import requests
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Optional, Any
|
||||
from adif_auth import AdifAuthenticator
|
||||
|
||||
|
||||
class AdifClient:
|
||||
"""Cliente para interactuar con la API de ADIF"""
|
||||
|
||||
def __init__(self, access_key: str, secret_key: str):
|
||||
"""
|
||||
Inicializa el cliente
|
||||
|
||||
Args:
|
||||
access_key: Clave de acceso
|
||||
secret_key: Clave secreta
|
||||
"""
|
||||
self.auth = AdifAuthenticator(access_key=access_key, secret_key=secret_key)
|
||||
self.session = requests.Session()
|
||||
|
||||
def _make_request(
|
||||
self,
|
||||
url: str,
|
||||
payload: Dict[str, Any],
|
||||
use_stations_key: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Realiza una petición a la API
|
||||
|
||||
Args:
|
||||
url: URL del endpoint
|
||||
payload: Datos a enviar
|
||||
use_stations_key: Si True, usa USER_KEY_STATIONS en lugar de USER_KEY_CIRCULATION
|
||||
|
||||
Returns:
|
||||
Respuesta JSON
|
||||
|
||||
Raises:
|
||||
Exception: Si hay un error en la petición
|
||||
"""
|
||||
user_id = str(uuid.uuid4())
|
||||
headers = self.auth.get_auth_headers("POST", url, payload, user_id=user_id)
|
||||
|
||||
if use_stations_key:
|
||||
headers["User-key"] = self.auth.USER_KEY_STATIONS
|
||||
else:
|
||||
headers["User-key"] = self.auth.USER_KEY_CIRCULATION
|
||||
|
||||
response = self.session.post(url, json=payload, headers=headers, timeout=30)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
elif response.status_code == 204:
|
||||
return {"message": "No content available", "commercialPaths": []}
|
||||
elif response.status_code == 401:
|
||||
raise PermissionError(
|
||||
f"Unauthorized - Las claves no tienen permisos para este endpoint"
|
||||
)
|
||||
elif response.status_code == 400:
|
||||
raise ValueError(
|
||||
f"Bad Request - Payload incorrecto: {response.text}"
|
||||
)
|
||||
else:
|
||||
raise Exception(
|
||||
f"Error {response.status_code}: {response.text}"
|
||||
)
|
||||
|
||||
def get_departures(
|
||||
self,
|
||||
station_code: str,
|
||||
traffic_type: str = "ALL",
|
||||
page_number: int = 0,
|
||||
commercial_service: str = "BOTH",
|
||||
commercial_stop_type: str = "BOTH"
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Obtiene las salidas de una estación
|
||||
|
||||
Args:
|
||||
station_code: Código de la estación (ej: "10200")
|
||||
traffic_type: Tipo de tráfico (ALL, CERCANIAS, AVLDMD, TRAVELERS, GOODS)
|
||||
page_number: Número de página (por defecto 0)
|
||||
commercial_service: BOTH, YES, NOT
|
||||
commercial_stop_type: BOTH, YES, NOT
|
||||
|
||||
Returns:
|
||||
Lista de trenes
|
||||
|
||||
Example:
|
||||
>>> client = AdifClient(ACCESS_KEY, SECRET_KEY)
|
||||
>>> trains = client.get_departures("10200", "AVLDMD")
|
||||
>>> for train in trains:
|
||||
... print(f"{train['commercialNumber']} - Destino: {train['destination']}")
|
||||
"""
|
||||
url = "https://circulacion.api.adif.es/portroyalmanager/secure/circulationpaths/departures/traffictype/"
|
||||
payload = {
|
||||
"commercialService": commercial_service,
|
||||
"commercialStopType": commercial_stop_type,
|
||||
"page": {"pageNumber": page_number},
|
||||
"stationCode": station_code,
|
||||
"trafficType": traffic_type
|
||||
}
|
||||
|
||||
data = self._make_request(url, payload)
|
||||
return data.get("commercialPaths", [])
|
||||
|
||||
def get_arrivals(
|
||||
self,
|
||||
station_code: str,
|
||||
traffic_type: str = "ALL",
|
||||
page_number: int = 0,
|
||||
commercial_service: str = "BOTH",
|
||||
commercial_stop_type: str = "BOTH"
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Obtiene las llegadas a una estación
|
||||
|
||||
Args:
|
||||
station_code: Código de la estación (ej: "10200")
|
||||
traffic_type: Tipo de tráfico (ALL, CERCANIAS, AVLDMD, TRAVELERS, GOODS)
|
||||
page_number: Número de página (por defecto 0)
|
||||
commercial_service: BOTH, YES, NOT
|
||||
commercial_stop_type: BOTH, YES, NOT
|
||||
|
||||
Returns:
|
||||
Lista de trenes
|
||||
|
||||
Example:
|
||||
>>> client = AdifClient(ACCESS_KEY, SECRET_KEY)
|
||||
>>> trains = client.get_arrivals("71801", "ALL")
|
||||
"""
|
||||
url = "https://circulacion.api.adif.es/portroyalmanager/secure/circulationpaths/arrivals/traffictype/"
|
||||
payload = {
|
||||
"commercialService": commercial_service,
|
||||
"commercialStopType": commercial_stop_type,
|
||||
"page": {"pageNumber": page_number},
|
||||
"stationCode": station_code,
|
||||
"trafficType": traffic_type
|
||||
}
|
||||
|
||||
data = self._make_request(url, payload)
|
||||
return data.get("commercialPaths", [])
|
||||
|
||||
def get_train_route(
|
||||
self,
|
||||
commercial_number: str,
|
||||
launching_date: int,
|
||||
origin_station_code: str,
|
||||
destination_station_code: str,
|
||||
all_control_points: bool = True
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Obtiene la ruta completa de un tren (todas las paradas)
|
||||
|
||||
Args:
|
||||
commercial_number: Número comercial del tren (ej: "03194")
|
||||
launching_date: Fecha de salida en milisegundos desde epoch
|
||||
origin_station_code: Código de estación de origen
|
||||
destination_station_code: Código de estación de destino
|
||||
all_control_points: Si True, incluye todos los puntos de control
|
||||
|
||||
Returns:
|
||||
Lista de paradas del tren
|
||||
|
||||
Example:
|
||||
>>> # Primero obtener un tren real
|
||||
>>> trains = client.get_departures("10200", "AVLDMD")
|
||||
>>> train = trains[0]
|
||||
>>> info = train['commercialPathInfo']
|
||||
>>> key = info['commercialPathKey']
|
||||
>>>
|
||||
>>> # Obtener su ruta completa
|
||||
>>> route = client.get_train_route(
|
||||
... commercial_number=key['commercialCirculationKey']['commercialNumber'],
|
||||
... launching_date=key['commercialCirculationKey']['launchingDate'],
|
||||
... origin_station_code=key['originStationCode'],
|
||||
... destination_station_code=key['destinationStationCode']
|
||||
... )
|
||||
>>> for stop in route:
|
||||
... print(f"Parada: {stop['stationCode']}")
|
||||
"""
|
||||
url = "https://circulacion.api.adif.es/portroyalmanager/secure/circulationpathdetails/onepaths/"
|
||||
payload = {
|
||||
"allControlPoints": all_control_points,
|
||||
"commercialNumber": commercial_number,
|
||||
"destinationStationCode": destination_station_code,
|
||||
"launchingDate": launching_date,
|
||||
"originStationCode": origin_station_code
|
||||
}
|
||||
|
||||
data = self._make_request(url, payload)
|
||||
commercial_paths = data.get("commercialPaths", [])
|
||||
|
||||
if commercial_paths:
|
||||
return commercial_paths[0].get("passthroughSteps", [])
|
||||
return []
|
||||
|
||||
def get_station_observations(
|
||||
self,
|
||||
station_codes: List[str]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Obtiene observaciones de estaciones
|
||||
|
||||
Args:
|
||||
station_codes: Lista de códigos de estación
|
||||
|
||||
Returns:
|
||||
Lista de observaciones
|
||||
|
||||
Example:
|
||||
>>> client = AdifClient(ACCESS_KEY, SECRET_KEY)
|
||||
>>> obs = client.get_station_observations(["10200", "71801"])
|
||||
"""
|
||||
url = "https://estaciones.api.adif.es/portroyalmanager/secure/stationsobservations/"
|
||||
payload = {"stationCodes": station_codes}
|
||||
|
||||
data = self._make_request(url, payload, use_stations_key=True)
|
||||
return data.get("stationObservations", [])
|
||||
|
||||
def get_all_departures_with_routes(
|
||||
self,
|
||||
station_code: str,
|
||||
traffic_type: str = "ALL",
|
||||
max_trains: int = 5
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Obtiene salidas de una estación Y sus rutas completas
|
||||
|
||||
Args:
|
||||
station_code: Código de estación
|
||||
traffic_type: Tipo de tráfico
|
||||
max_trains: Número máximo de trenes a procesar
|
||||
|
||||
Returns:
|
||||
Lista de trenes con sus rutas
|
||||
|
||||
Example:
|
||||
>>> client = AdifClient(ACCESS_KEY, SECRET_KEY)
|
||||
>>> trains_with_routes = client.get_all_departures_with_routes("10200", "AVLDMD", max_trains=3)
|
||||
>>> for train in trains_with_routes:
|
||||
... print(f"Tren {train['commercial_number']}")
|
||||
... for stop in train['route']:
|
||||
... print(f" - {stop['stationCode']}")
|
||||
"""
|
||||
departures = self.get_departures(station_code, traffic_type)
|
||||
result = []
|
||||
|
||||
for i, train in enumerate(departures[:max_trains]):
|
||||
info = train['commercialPathInfo']
|
||||
key = info['commercialPathKey']
|
||||
commercial_key = key['commercialCirculationKey']
|
||||
|
||||
try:
|
||||
route = self.get_train_route(
|
||||
commercial_number=commercial_key['commercialNumber'],
|
||||
launching_date=commercial_key['launchingDate'],
|
||||
origin_station_code=key['originStationCode'],
|
||||
destination_station_code=key['destinationStationCode']
|
||||
)
|
||||
|
||||
result.append({
|
||||
"commercial_number": commercial_key['commercialNumber'],
|
||||
"traffic_type": info['trafficType'],
|
||||
"origin_station": key['originStationCode'],
|
||||
"destination_station": key['destinationStationCode'],
|
||||
"launching_date": commercial_key['launchingDate'],
|
||||
"train_info": train,
|
||||
"route": route
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error obteniendo ruta del tren {commercial_key['commercialNumber']}: {e}")
|
||||
continue
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def demo():
|
||||
"""Demostración del cliente"""
|
||||
print("="*70)
|
||||
print("DEMO DEL CLIENTE DE ADIF")
|
||||
print("="*70)
|
||||
|
||||
ACCESS_KEY = "and20210615"
|
||||
SECRET_KEY = "Jthjtr946RTt"
|
||||
|
||||
client = AdifClient(ACCESS_KEY, SECRET_KEY)
|
||||
|
||||
# 1. Salidas de Madrid Atocha
|
||||
print("\n1️⃣ SALIDAS DE MADRID ATOCHA (Alta Velocidad)")
|
||||
print("-" * 70)
|
||||
try:
|
||||
departures = client.get_departures("10200", "AVLDMD")
|
||||
print(f"✅ Encontrados {len(departures)} trenes")
|
||||
|
||||
for i, train in enumerate(departures[:3]):
|
||||
info = train['commercialPathInfo']
|
||||
key = info['commercialPathKey']
|
||||
passthrough = train.get('passthroughStep', {})
|
||||
dep_sides = passthrough.get('departurePassthroughStepSides', {})
|
||||
|
||||
planned_time = dep_sides.get('plannedTime', 0)
|
||||
if planned_time:
|
||||
time_str = datetime.fromtimestamp(planned_time/1000).strftime('%H:%M')
|
||||
else:
|
||||
time_str = "N/A"
|
||||
|
||||
print(f"\n {i+1}. Tren {key['commercialCirculationKey']['commercialNumber']}")
|
||||
print(f" Destino: {key['destinationStationCode']}")
|
||||
print(f" Hora salida: {time_str}")
|
||||
print(f" Estado: {dep_sides.get('circulationState', 'N/A')}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
|
||||
# 2. Ruta completa de un tren
|
||||
print("\n\n2️⃣ RUTA COMPLETA DE UN TREN")
|
||||
print("-" * 70)
|
||||
try:
|
||||
departures = client.get_departures("10200", "ALL")
|
||||
if departures:
|
||||
train = departures[0]
|
||||
info = train['commercialPathInfo']
|
||||
key = info['commercialPathKey']
|
||||
commercial_key = key['commercialCirculationKey']
|
||||
|
||||
print(f"Consultando ruta del tren {commercial_key['commercialNumber']}...")
|
||||
|
||||
route = client.get_train_route(
|
||||
commercial_number=commercial_key['commercialNumber'],
|
||||
launching_date=commercial_key['launchingDate'],
|
||||
origin_station_code=key['originStationCode'],
|
||||
destination_station_code=key['destinationStationCode']
|
||||
)
|
||||
|
||||
print(f"✅ Ruta con {len(route)} paradas:\n")
|
||||
for i, stop in enumerate(route[:10]): # Primeras 10 paradas
|
||||
stop_type = stop.get('stopType', 'N/A')
|
||||
station_code = stop.get('stationCode', 'N/A')
|
||||
|
||||
# Info de salida
|
||||
dep_sides = stop.get('departurePassthroughStepSides', {})
|
||||
arr_sides = stop.get('arrivalPassthroughStepSides', {})
|
||||
|
||||
if dep_sides:
|
||||
time_ms = dep_sides.get('plannedTime', 0)
|
||||
if time_ms:
|
||||
time_str = datetime.fromtimestamp(time_ms/1000).strftime('%H:%M')
|
||||
print(f" {i+1}. {station_code} - Salida: {time_str} ({stop_type})")
|
||||
elif arr_sides:
|
||||
time_ms = arr_sides.get('plannedTime', 0)
|
||||
if time_ms:
|
||||
time_str = datetime.fromtimestamp(time_ms/1000).strftime('%H:%M')
|
||||
print(f" {i+1}. {station_code} - Llegada: {time_str} ({stop_type})")
|
||||
else:
|
||||
print(f" {i+1}. {station_code} ({stop_type})")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
|
||||
# 3. Observaciones de estaciones
|
||||
print("\n\n3️⃣ OBSERVACIONES DE ESTACIONES")
|
||||
print("-" * 70)
|
||||
try:
|
||||
observations = client.get_station_observations(["10200", "71801"])
|
||||
print(f"✅ Observaciones de {len(observations)} estaciones")
|
||||
|
||||
for obs in observations:
|
||||
station_code = obs.get('stationCode', 'N/A')
|
||||
observation_text = obs.get('observation', 'Sin observaciones')
|
||||
print(f"\n Estación {station_code}:")
|
||||
print(f" {observation_text}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("DEMO COMPLETADA")
|
||||
print("="*70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
demo()
|
||||
440
apk_decompiled/resources/AndroidManifest.xml
Normal file
440
apk_decompiled/resources/AndroidManifest.xml
Normal file
@@ -0,0 +1,440 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:versionCode="72"
|
||||
android:versionName="2.1.0"
|
||||
android:compileSdkVersion="35"
|
||||
android:compileSdkVersionCodename="15"
|
||||
package="com.adif.elcanomovil"
|
||||
platformBuildVersionCode="35"
|
||||
platformBuildVersionName="15">
|
||||
<uses-sdk
|
||||
android:minSdkVersion="29"
|
||||
android:targetSdkVersion="35"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="com.google.android.gms.permission.AD_ID"/>
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
<package android:name="com.google.android.apps.maps"/>
|
||||
</queries>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-feature
|
||||
android:glEsVersion="0x20000"
|
||||
android:required="true"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
|
||||
<uses-permission android:name="com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE"/>
|
||||
<permission
|
||||
android:name="com.adif.elcanomovil.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
|
||||
android:protectionLevel="signature"/>
|
||||
<uses-permission android:name="com.adif.elcanomovil.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"/>
|
||||
<application
|
||||
android:theme="@style/SplashScreenTheme"
|
||||
android:label="@string/app_name_label"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:name="com.adif.elcanomovil.ElcanoApplication"
|
||||
android:allowBackup="false"
|
||||
android:hardwareAccelerated="true"
|
||||
android:supportsRtl="true"
|
||||
android:extractNativeLibs="false"
|
||||
android:usesCleartextTraffic="false"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:appComponentFactory="androidx.core.app.CoreComponentFactory"
|
||||
android:requestLegacyExternalStorage="true">
|
||||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
android:value="AIzaSyDIzMtgIKRHGwmOFihX_--ftSeMLjwF3cY"/>
|
||||
<activity
|
||||
android:name="com.adif.elcanomovil.main.MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleInstance"
|
||||
android:windowSoftInputMode="adjustPan">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="adif.page.link"/>
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="adifpreproduccion.page.link"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="adifmovil"/>
|
||||
<data android:host="app"/>
|
||||
<data android:path="/home"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="adifmovil"/>
|
||||
<data android:host="app"/>
|
||||
<data android:path="/departures"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="adifmovil"/>
|
||||
<data android:host="app"/>
|
||||
<data android:path="/station"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="adifmovil"/>
|
||||
<data android:host="app"/>
|
||||
<data android:path="/moreAdif"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="adifmovil"/>
|
||||
<data android:host="app"/>
|
||||
<data android:path="/selectTrain"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="adifmovil"/>
|
||||
<data android:host="app"/>
|
||||
<data android:path="/favourites"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="adifmovil"/>
|
||||
<data android:host="app"/>
|
||||
<data android:path="/avisa"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="adifmovil"/>
|
||||
<data android:host="app"/>
|
||||
<data android:path="/avisaIncidenceDetails"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="adifmovil"/>
|
||||
<data android:host="app"/>
|
||||
<data android:path="/subscriptionDetails"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="adifmovil"/>
|
||||
<data android:host="app"/>
|
||||
<data android:path="/trainDetails"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="adifmovil"/>
|
||||
<data android:host="app"/>
|
||||
<data android:path="/favourites"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="adifmovil"/>
|
||||
<data android:host="app"/>
|
||||
<data android:path="/avisa"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="adifmovil"/>
|
||||
<data android:host="app"/>
|
||||
<data android:path="/avisaIncidenceDetails"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="adifmovil"/>
|
||||
<data android:host="app"/>
|
||||
<data android:path="/subscriptionDetails"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="adifmovil"/>
|
||||
<data android:host="app"/>
|
||||
<data android:path="/trainDetails"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:label="WizardActivity"
|
||||
android:name="com.adif.elcanomovil.uiMoreAdif.wizard.WizardActivity"
|
||||
android:exported="false"
|
||||
android:screenOrientation="portrait">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_icon"
|
||||
android:resource="@drawable/ic_adif_logo_simple"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_color"
|
||||
android:resource="@color/colorPrimary"/>
|
||||
<meta-data
|
||||
android:name="com.adif.elcanomovil.notifications.AppFirebaseMessagingService"
|
||||
android:value="@string/default_notification_channel_id"/>
|
||||
<service
|
||||
android:name="com.adif.elcanomovil.notifications.AppFirebaseMessagingService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
<receiver
|
||||
android:name="com.adif.elcanomovil.widget.DeparturesWidget"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/departures_widget_info"/>
|
||||
</receiver>
|
||||
<activity
|
||||
android:name="com.adif.elcanomovil.widget.DeparturesWidgetConfigureActivity"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:exported="false"
|
||||
android:authorities="com.adif.elcanomovil.provider"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"/>
|
||||
</provider>
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:exported="false"
|
||||
android:authorities="com.adif.elcanomovil.androidx-startup">
|
||||
<meta-data
|
||||
android:name="androidx.emoji2.text.EmojiCompatInitializer"
|
||||
android:value="androidx.startup"/>
|
||||
<meta-data
|
||||
android:name="androidx.lifecycle.ProcessLifecycleInitializer"
|
||||
android:value="androidx.startup"/>
|
||||
<meta-data
|
||||
android:name="androidx.profileinstaller.ProfileInstallerInitializer"
|
||||
android:value="androidx.startup"/>
|
||||
</provider>
|
||||
<uses-library
|
||||
android:name="org.apache.http.legacy"
|
||||
android:required="false"/>
|
||||
<service
|
||||
android:name="com.google.firebase.components.ComponentDiscoveryService"
|
||||
android:exported="false"
|
||||
android:directBootAware="true">
|
||||
<meta-data
|
||||
android:name="com.google.firebase.components:com.google.firebase.messaging.ktx.FirebaseMessagingLegacyRegistrar"
|
||||
android:value="com.google.firebase.components.ComponentRegistrar"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.components:com.google.firebase.messaging.FirebaseMessagingKtxRegistrar"
|
||||
android:value="com.google.firebase.components.ComponentRegistrar"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.components:com.google.firebase.messaging.FirebaseMessagingRegistrar"
|
||||
android:value="com.google.firebase.components.ComponentRegistrar"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.components:com.google.firebase.dynamiclinks.ktx.FirebaseDynamicLinksLegacyRegistrar"
|
||||
android:value="com.google.firebase.components.ComponentRegistrar"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.components:com.google.firebase.dynamiclinks.FirebaseDynamicLinksKtxRegistrar"
|
||||
android:value="com.google.firebase.components.ComponentRegistrar"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.components:com.google.firebase.dynamiclinks.internal.FirebaseDynamicLinkRegistrar"
|
||||
android:value="com.google.firebase.components.ComponentRegistrar"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.components:com.google.firebase.storage.ktx.FirebaseStorageKtxRegistrar"
|
||||
android:value="com.google.firebase.components.ComponentRegistrar"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.components:com.google.firebase.storage.StorageRegistrar"
|
||||
android:value="com.google.firebase.components.ComponentRegistrar"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.components:com.google.firebase.remoteconfig.ktx.FirebaseConfigLegacyRegistrar"
|
||||
android:value="com.google.firebase.components.ComponentRegistrar"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.components:com.google.firebase.crashlytics.FirebaseCrashlyticsKtxRegistrar"
|
||||
android:value="com.google.firebase.components.ComponentRegistrar"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.components:com.google.firebase.crashlytics.CrashlyticsRegistrar"
|
||||
android:value="com.google.firebase.components.ComponentRegistrar"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.components:com.google.firebase.analytics.ktx.FirebaseAnalyticsKtxRegistrar"
|
||||
android:value="com.google.firebase.components.ComponentRegistrar"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.components:com.google.firebase.remoteconfig.FirebaseRemoteConfigKtxRegistrar"
|
||||
android:value="com.google.firebase.components.ComponentRegistrar"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.components:com.google.firebase.remoteconfig.RemoteConfigRegistrar"
|
||||
android:value="com.google.firebase.components.ComponentRegistrar"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.components:com.google.firebase.sessions.FirebaseSessionsRegistrar"
|
||||
android:value="com.google.firebase.components.ComponentRegistrar"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.components:com.google.firebase.analytics.connector.internal.AnalyticsConnectorRegistrar"
|
||||
android:value="com.google.firebase.components.ComponentRegistrar"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.components:com.google.firebase.installations.FirebaseInstallationsKtxRegistrar"
|
||||
android:value="com.google.firebase.components.ComponentRegistrar"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.components:com.google.firebase.installations.FirebaseInstallationsRegistrar"
|
||||
android:value="com.google.firebase.components.ComponentRegistrar"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.components:com.google.firebase.ktx.FirebaseCommonLegacyRegistrar"
|
||||
android:value="com.google.firebase.components.ComponentRegistrar"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.components:com.google.firebase.FirebaseCommonKtxRegistrar"
|
||||
android:value="com.google.firebase.components.ComponentRegistrar"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.components:com.google.firebase.abt.component.AbtRegistrar"
|
||||
android:value="com.google.firebase.components.ComponentRegistrar"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.components:com.google.firebase.datatransport.TransportRegistrar"
|
||||
android:value="com.google.firebase.components.ComponentRegistrar"/>
|
||||
</service>
|
||||
<receiver
|
||||
android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver"
|
||||
android:permission="com.google.android.c2dm.permission.SEND"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.c2dm.intent.RECEIVE"/>
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.cloudmessaging.FINISHED_AFTER_HANDLED"
|
||||
android:value="true"/>
|
||||
</receiver>
|
||||
<service
|
||||
android:name="com.google.firebase.messaging.FirebaseMessagingService"
|
||||
android:exported="false"
|
||||
android:directBootAware="true">
|
||||
<intent-filter android:priority="-500">
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
<activity
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:name="com.google.android.gms.common.api.GoogleApiActivity"
|
||||
android:exported="false"/>
|
||||
<service
|
||||
android:name="com.google.firebase.sessions.SessionLifecycleService"
|
||||
android:enabled="true"
|
||||
android:exported="false"/>
|
||||
<provider
|
||||
android:name="com.google.firebase.provider.FirebaseInitProvider"
|
||||
android:exported="false"
|
||||
android:authorities="com.adif.elcanomovil.firebaseinitprovider"
|
||||
android:initOrder="100"
|
||||
android:directBootAware="true"/>
|
||||
<receiver
|
||||
android:name="com.google.android.gms.measurement.AppMeasurementReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false"/>
|
||||
<service
|
||||
android:name="com.google.android.gms.measurement.AppMeasurementService"
|
||||
android:enabled="true"
|
||||
android:exported="false"/>
|
||||
<service
|
||||
android:name="com.google.android.gms.measurement.AppMeasurementJobService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
android:enabled="true"
|
||||
android:exported="false"/>
|
||||
<service
|
||||
android:name="androidx.room.MultiInstanceInvalidationService"
|
||||
android:exported="false"
|
||||
android:directBootAware="true"/>
|
||||
<uses-library
|
||||
android:name="androidx.window.extensions"
|
||||
android:required="false"/>
|
||||
<uses-library
|
||||
android:name="androidx.window.sidecar"
|
||||
android:required="false"/>
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.version"
|
||||
android:value="@integer/google_play_services_version"/>
|
||||
<service
|
||||
android:name="androidx.core.widget.RemoteViewsCompatService"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS"/>
|
||||
<receiver
|
||||
android:name="androidx.profileinstaller.ProfileInstallReceiver"
|
||||
android:permission="android.permission.DUMP"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:directBootAware="false">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.profileinstaller.action.INSTALL_PROFILE"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="androidx.profileinstaller.action.SKIP_FILE"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="androidx.profileinstaller.action.SAVE_PROFILE"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="androidx.profileinstaller.action.BENCHMARK_OPERATION"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<service
|
||||
android:name="com.google.android.datatransport.runtime.backends.TransportBackendDiscovery"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="backend:com.google.android.datatransport.cct.CctBackendFactory"
|
||||
android:value="cct"/>
|
||||
</service>
|
||||
<service
|
||||
android:name="com.google.android.datatransport.runtime.scheduling.jobscheduling.JobInfoSchedulerService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
android:exported="false"/>
|
||||
<receiver
|
||||
android:name="com.google.android.datatransport.runtime.scheduling.jobscheduling.AlarmManagerSchedulerBroadcastReceiver"
|
||||
android:exported="false"/>
|
||||
</application>
|
||||
</manifest>
|
||||
BIN
apk_decompiled/resources/DebugProbesKt.bin
Normal file
BIN
apk_decompiled/resources/DebugProbesKt.bin
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
1.9.0
|
||||
@@ -0,0 +1 @@
|
||||
1.9.0
|
||||
@@ -0,0 +1 @@
|
||||
1.4.0
|
||||
@@ -0,0 +1 @@
|
||||
1.7.0
|
||||
@@ -0,0 +1 @@
|
||||
1.7.0
|
||||
@@ -0,0 +1 @@
|
||||
task ':arch:core:core-runtime:writeVersionFile' property 'version'
|
||||
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
@@ -0,0 +1 @@
|
||||
1.1.0
|
||||
@@ -0,0 +1 @@
|
||||
1.13.0
|
||||
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
@@ -0,0 +1 @@
|
||||
1.13.0
|
||||
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
@@ -0,0 +1 @@
|
||||
1.1.0
|
||||
@@ -0,0 +1 @@
|
||||
8.9.0
|
||||
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
@@ -0,0 +1 @@
|
||||
1.1.1
|
||||
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
@@ -0,0 +1 @@
|
||||
1.3.0
|
||||
@@ -0,0 +1 @@
|
||||
1.3.0
|
||||
@@ -0,0 +1 @@
|
||||
1.3.6
|
||||
@@ -0,0 +1 @@
|
||||
1.7.0
|
||||
@@ -0,0 +1 @@
|
||||
1.7.0
|
||||
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
@@ -0,0 +1 @@
|
||||
2.8.0
|
||||
@@ -0,0 +1 @@
|
||||
2.8.0
|
||||
@@ -0,0 +1 @@
|
||||
2.8.0
|
||||
@@ -0,0 +1 @@
|
||||
2.8.0
|
||||
@@ -0,0 +1 @@
|
||||
2.8.0
|
||||
@@ -0,0 +1 @@
|
||||
2.8.0
|
||||
@@ -0,0 +1 @@
|
||||
2.8.0
|
||||
@@ -0,0 +1 @@
|
||||
2.8.0
|
||||
@@ -0,0 +1 @@
|
||||
2.8.0
|
||||
@@ -0,0 +1 @@
|
||||
2.8.0
|
||||
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
@@ -0,0 +1 @@
|
||||
2.5.3
|
||||
@@ -0,0 +1 @@
|
||||
2.5.3
|
||||
@@ -0,0 +1 @@
|
||||
2.5.3
|
||||
@@ -0,0 +1 @@
|
||||
2.5.3
|
||||
@@ -0,0 +1 @@
|
||||
2.5.3
|
||||
@@ -0,0 +1 @@
|
||||
2.5.3
|
||||
@@ -0,0 +1 @@
|
||||
2.5.3
|
||||
@@ -0,0 +1 @@
|
||||
2.5.3
|
||||
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
@@ -0,0 +1 @@
|
||||
1.3.1
|
||||
@@ -0,0 +1 @@
|
||||
1.3.2
|
||||
@@ -0,0 +1 @@
|
||||
2.6.1
|
||||
@@ -0,0 +1 @@
|
||||
2.6.1
|
||||
@@ -0,0 +1 @@
|
||||
1.2.1
|
||||
@@ -0,0 +1 @@
|
||||
1.2.1
|
||||
@@ -0,0 +1 @@
|
||||
1.2.0
|
||||
@@ -0,0 +1 @@
|
||||
2.4.0
|
||||
@@ -0,0 +1 @@
|
||||
2.4.0
|
||||
@@ -0,0 +1 @@
|
||||
1.1.1
|
||||
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
@@ -0,0 +1 @@
|
||||
1.5.0
|
||||
@@ -0,0 +1 @@
|
||||
1.1.0
|
||||
@@ -0,0 +1 @@
|
||||
1.1.0
|
||||
@@ -0,0 +1 @@
|
||||
1.1.1
|
||||
@@ -0,0 +1 @@
|
||||
1.1.0-beta02
|
||||
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
1.12.0
|
||||
@@ -0,0 +1 @@
|
||||
2.50
|
||||
@@ -0,0 +1 @@
|
||||
2.50
|
||||
@@ -0,0 +1 @@
|
||||
2.50
|
||||
@@ -0,0 +1 @@
|
||||
2.50
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
appMetadataVersion=1.1
|
||||
androidGradlePluginVersion=8.9.0
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user