diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 2bc52a0..0000000 --- a/Dockerfile +++ /dev/null @@ -1,57 +0,0 @@ -FROM openjdk:17-jdk-slim - -# Evitar interactividad durante la instalación -ENV DEBIAN_FRONTEND=noninteractive - -# Instalar dependencias necesarias para Android SDK -RUN apt-get update && apt-get install -y \ - wget \ - unzip \ - git \ - python3 \ - python3-pip \ - ncurses-bin \ - build-essential \ - lib32z1 \ - lib32ncurses6 \ - lib32stdc++6 \ - zlib1g-dev \ - && rm -rf /var/lib/apt/lists/* - -# Instalar Android SDK -ENV ANDROID_SDK_ROOT=/opt/android-sdk -ENV SDKMANAGER="$ANDROID_SDK_ROOT/cmdline-tools/bin/sdkmanager" - -RUN mkdir -p $ANDROID_SDK_ROOT/cmdline-tools && \ - wget -q https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip -O tools.zip && \ - unzip -q tools.zip && \ - mv cmdline-tools $ANDROID_SDK_ROOT/cmdline-tools/latest && \ - rm tools.zip - -# Aceptar licencias -RUN yes | $SDKMANAGER --licenses - -# Instalar componentes necesarios -RUN $SDKMANAGER "platform-tools" "platforms;android-33" "build-tools;33.0.2" "platforms;android-31" - -# Instalar Gradle -ENV GRADLE_HOME=/opt/gradle -RUN wget -q https://services.gradle.org/distributions/gradle-8.2-bin.zip -O gradle.zip && \ - unzip -q gradle.zip && \ - mv gradle-8.2 $GRADLE_HOME && \ - rm gradle.zip - -ENV PATH=$PATH:$GRADLE_HOME/bin:$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$ANDROID_SDK_ROOT/platform-tools - -# Copiar proyecto -COPY . /app -WORKDIR /app - -# Dar permisos de ejecución a gradlew -RUN chmod +x ./gradlew - -# Construir APK -RUN ./gradlew assembleDebug - -# Comando para copiar APK a un volumen montado -CMD ["cp", "/app/app/build/outputs/apk/debug/app-debug.apk", "/output/streamplayer.apk"] \ No newline at end of file diff --git a/README.md b/README.md index b1c5798..ff350b6 100644 --- a/README.md +++ b/README.md @@ -1,306 +1,54 @@ -# 📺 StreamPlayer +## StreamPlayer Desktop (Windows) -[![Android](https://img.shields.io/badge/Platform-Android-green.svg)](https://android.com) -[![Java](https://img.shields.io/badge/Language-Java-orange.svg)](https://www.oracle.com/java/) -[![API](https://img.shields.io/badge/Min%20SDK-21%2B-brightgreen.svg)](https://android-developers.blogspot.com/) -[![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +Este branch contiene únicamente la versión de escritorio desarrollada con **.NET 8 + Avalonia**. Replica todas las funciones del APK Android original (resolución de streams ofuscados, verificación remota de dispositivos, control de actualizaciones y reproducción protegida) pero genera un `.exe` listo para Windows. -Aplicación Android reproductora de streaming con optimización DNS para mejor rendimiento y acceso a contenido multimedia. +### Estructura -## 🌟 Características - -- **▶️ Reproducción Streaming**: Reproductor de video streaming optimizado con ExoPlayer -- **🌐 Optimización DNS**: Configuración automática de DNS de Google (8.8.8.8, 8.8.4.4) para mejor conectividad -- **🔍 Resolución de URL**: Sistema avanzado que resuelve URLs ofuscadas de streaming -- **📱 Orientación Landscape**: Diseño optimizado para experiencia multimedia inmersiva -- **⚡ Alto Rendimiento**: Implementación asíncrona para respuesta rápida -- **🛡️ Manejo de Errores**: Sistema robusto de gestión de errores y estados - -## 📋 Requisitos - -- **Android SDK**: API 21 (Android 5.0) o superior -- **Target SDK**: API 33 (Android 13) -- **Permisos**: - - `INTERNET` - Acceso a streaming - - `ACCESS_NETWORK_STATE` - Verificación de conectividad - - `CHANGE_NETWORK_STATE` - Configuración de red - -## 🏗️ Arquitectura - -### Componentes Principales - -- **MainActivity.java** (`/app/src/main/java/com/streamplayer/MainActivity.java`) - - Gestión del ciclo de vida del reproductor - - Configuración de ExoPlayer - - Manejo de estados (loading, error, reproducción) - -- **StreamUrlResolver.java** (`/app/src/main/java/com/streamplayer/StreamUrlResolver.java`) - - Resolución de URLs ofuscadas - - Decodificación Base64 - - Extracción de claves de JavaScript - -- **DNSSetter.java** (`/app/src/main/java/com/streamplayer/DNSSetter.java`) - - Configuración de DNS de Google - - Optimización de red para streaming - - Pre-resolución de dominios - -## 🚀 Instalación y Build - -### Prerequisites -```bash -# Android SDK -# Java 8+ -# Gradle 8.2+ +``` +windows/StreamPlayer.Desktop/ +├── App.axaml / App.axaml.cs +├── Program.cs +├── AppVersion.cs +├── Models/ # ChannelSection, LiveEvent, UpdateInfo, etc. +├── Services/ # StreamUrlResolver, UpdateService, DeviceRegistryService, WindowsDnsService… +├── ViewModels/ # MainWindowViewModel con bloqueo previo y refrescos +├── Views/ # MainWindow, PlayerWindow (LibVLC), diálogos de update/bloqueo +└── StreamPlayer.Desktop.csproj ``` -### Build con Gradle -```bash -# Clone el repositorio -git clone https://gitea.cbcren.online/renato97/app.git -cd app +### Requisitos -# Build APK debug -./gradlew assembleDebug +- .NET SDK 8.0 +- Windows 10/11 x64 +- Visual Studio 2022 (o `dotnet` CLI) con workloads “.NET desktop”. +- VLC runtimes incluidos vía `VideoLAN.LibVLC.Windows`. -# Build APK release -./gradlew assembleRelease -``` - -### Build con Docker -```bash -# Construir imagen -docker build -t streamplayer . - -# Ejecutar build -docker run --rm -v $(pwd)/output:/output streamplayer -``` - -### Build Script Alternativo -```bash -# Usar script de build -chmod +x build_apk.sh -./build_apk.sh -``` - -## 🔄 Control de Instalaciones y Actualizaciones - -StreamPlayer ahora consulta automáticamente las releases públicas del repositorio Gitea y puede forzar o sugerir actualizaciones directamente desde la app. Para aprovecharlo: - -1. Ejecuta un build nuevo incrementando `versionCode`/`versionName` en `app/build.gradle`. -2. Crea una release en Gitea asignando un tag como `v9.1` y sube el APK. -3. Adjunta un archivo `update-manifest.json` en la misma release (puedes partir de `release/update-manifest.example.json`). - -### Formato de `update-manifest.json` - -```json -{ - "versionCode": 91000, - "versionName": "9.1.0", - "minSupportedVersionCode": 90000, - "forceUpdate": false, - "downloadUrl": "https://gitea.cbcren.online/renato97/app/releases/download/v9.1/StreamPlayer-v9.1.apk", - "fileName": "StreamPlayer-v9.1.apk", - "sizeBytes": 12345678, - "notes": "Novedades destacadas que aparecerán en el diálogo dentro de la app" -} -``` - -- `versionCode` / `versionName`: deben coincidir con el APK publicado. -- `minSupportedVersionCode`: define desde qué versión mínima se permite seguir usando la app (ideal para bloquear instalaciones antiguas). -- `forceUpdate`: si es `true` la app mostrará un diálogo sin opción de omitir. -- `downloadUrl` / `fileName`: apuntan al asset `.apk` publicado en Gitea (puede ser el enlace de descarga directo). -- `notes`: texto libre mostrado en el diálogo dentro de la app; si lo omitís se usará el cuerpo de la release. - -Si por algún motivo olvidas subir el manifiesto, la app igualmente tomará el primer asset `.apk` de la release, pero no podrá forzar versiones mínimas. - -### Dashboard de Dispositivos y Bloqueo Remoto - -Para saber en qué equipo está instalada la app y bloquear el acceso cuando lo necesites, se incluye un dashboard liviano en `dashboard/`: - -1. Instala dependencias y ejecuta el servidor: +### Cómo compilar ```bash -cd dashboard -npm install -npm start # escucha en http://localhost:4000 +git checkout windows-only +cd windows/StreamPlayer.Desktop +dotnet restore +dotnet build -c Release +# Para distribuir: +dotnet publish -c Release -r win-x64 --self-contained true /p:PublishSingleFile=true ``` -2. Copia `dashboard/config.example.json` a `dashboard/config.json` y completa `telegramBotToken` + `telegramChatId` (o usa variables de entorno `TELEGRAM_BOT_TOKEN` / `TELEGRAM_CHAT_ID`). -3. Ajusta `DEVICE_REGISTRY_URL` en `app/build.gradle` para apuntar al dominio/puerto donde despliegues el servidor (ya configurado como `http://194.163.191.200:4000`). -4. Distribuye el APK; cada instalación reportará `ANDROID_ID`, modelo, IP pública y país. -5. Entra a `http://TU_HOST:4000/` para ver el listado, asignar alias, bloquear/desbloquear o validar tokens. +El `.exe` resultante queda en `windows/StreamPlayer.Desktop/bin/Release/net8.0/win-x64/publish/`. -El servidor guarda los datos en `dashboard/data/devices.json`, por lo que puedes versionarlo o respaldarlo fácilmente. Cada registro almacena: +### Características clave -- `deviceId`: `Settings.Secure.ANDROID_ID` del equipo -- `deviceName`, `manufacturer`, `model`, `osVersion` -- `appVersionName`/`Code` -- `ip`, `country` detectados automáticamente -- `firstSeen`, `lastSeen`, `blocked`, `notes`, `verification.status` +- **Resolución de stream**: `Services/StreamUrlResolver.cs` analiza el JavaScript ofuscado y reconstruye el HLS real (idéntico al app móvil). +- **Reproducción**: `Views/PlayerWindow` usa LibVLC con los mismos headers/User-Agent del APK para evitar bloqueos de origen. +- **Verificación remota**: `DeviceRegistryService` sincroniza con tu dashboard y bloquea toda la UI hasta que el servidor permita el dispositivo. +- **Actualizaciones forzadas**: `UpdateService` consulta las releases de Gitea y puede abrir el browser o descargar la nueva versión. +- **DNS de Google**: `WindowsDnsService` fuerza 8.8.8.8 / 8.8.4.4 solicitando elevación (UAC); si el usuario rechaza, se muestra el mensaje para configurar manualmente antes de iniciar el player. -Cuando presionas “Bloquear”, la app recibe la respuesta `{"blocked": true}` y muestra un diálogo irreversible hasta que lo habilites. Esto añade una capa adicional de control aparte del sistema de actualizaciones. +### Uso -### Flujo dentro de la app y tokens divididos +1. Compila o publica el `.exe`. +2. Ejecuta una vez como administrador para que el cambio de DNS quede aplicado. +3. Configura tus endpoints (`AppVersion.DeviceRegistryUrl`, `LatestReleaseApi`) si necesitas apuntar a otros servicios. +4. Distribuye el `.exe` y sube releases/manifest igual que con el APK. -- Cada vez que se abre `MainActivity` se consulta `https://gitea.cbcren.online/api/v1/repos/renato97/app/releases/latest`. -- Si `versionCode` del servidor es mayor al instalado se muestra un diálogo para actualizar; el usuario puede abrir la release o descargarla directamente. -- Si `minSupportedVersionCode` es mayor al instalado la app bloqueará el uso hasta actualizar, cumpliendo con el requerimiento de controlar instalaciones. -- La descarga se gestiona con `DownloadManager` y, una vez completada, se lanza el instalador usando FileProvider. -- Mientras el dashboard mantenga un dispositivo "Pendiente" o "Bloqueado", la app muestra un diálogo con el motivo y la mitad del token que debe compartir la persona. - -Cada instalación genera un token interno dividido en dos: - -1. **Parte cliente**: se muestra en el diálogo del dispositivo bloqueado para que el usuario pueda copiarla. -2. **Parte admin**: llega al bot de Telegram configurado junto con la IP, país y datos del dispositivo. - -Para autorizar un dispositivo pendiente: - -1. Obtén la parte cliente desde el usuario (visible en pantalla). -2. Copia la parte admin del mensaje de Telegram. -3. En el dashboard presiona “Verificar token” e introduce ambas mitades. Si coinciden, el estado pasa a "Verificado" y la app se desbloquea automáticamente. -4. A partir de allí puedes bloquear/desbloquear manualmente cuando quieras. - -También puedes gestionar todo desde Telegram: - -- `/allow ` autoriza el dispositivo (verifica el token y lo desbloquea). -- `/deny [motivo]` lo bloquea con un motivo opcional. -- `/pending` lista los registros que aún esperan un token válido. - -Cada nuevo registro dispara una notificación de Telegram con la parte admin del token y recordatorios de esos comandos. - -## 📱 Estructura del Proyecto - -``` -app/ -├── src/main/ -│ ├── java/com/streamplayer/ -│ │ ├── MainActivity.java # Actividad principal -│ │ ├── StreamUrlResolver.java # Resolvedor de URLs -│ │ └── DNSSetter.java # Configuración DNS -│ ├── res/ -│ │ ├── layout/ -│ │ │ └── activity_main.xml # UI principal -│ │ ├── mipmap-*/ # Íconos de la app -│ │ ├── values/ -│ │ │ ├── strings.xml # Cadenas de texto -│ │ │ ├── colors.xml # Colores -│ │ │ └── themes.xml # Temas -│ │ └── xml/ # Configuraciones -│ └── AndroidManifest.xml # Manifiesto Android -├── build.gradle # Configuración Gradle -└── proguard-rules.pro # Reglas ProGuard -``` - -## ⚙️ Configuración - -### URL de Streaming -La aplicación está configurada por defecto para: -``` -https://streamtpmedia.com/global2.php?stream=espn -``` - -### Configuración DNS -```java -// DNS configurados automáticamente -String[] GOOGLE_DNS = {"8.8.8.8", "8.8.4.4"}; -``` - -## 🔧 Dependencias Principales - -- **ExoPlayer 2.18.7**: Motor de reproducción multimedia -- **AndroidX AppCompat 1.6.1**: Compatibilidad hacia atrás -- **ConstraintLayout 2.1.4**: Layout moderno y flexible - -## 🛠️ Desarrollo - -### Flujo de Reproducción -1. **MainActivity** inicializa y configura DNS de Google -2. **StreamUrlResolver** obtiene y decodifica la URL real del stream -3. **ExoPlayer** inicia la reproducción con la URL resuelta -4. UI actualiza estados (loading, playing, error) - -### Características Técnicas -- **Threading**: Operaciones de red en background thread -- **Memory Management**: Proper lifecycle management de ExoPlayer -- **Error Handling**: Captura y display de errores al usuario -- **Network Optimization**: Configuración DNS específica para streaming - -## 📊 Build Configuration - -| Atributo | Valor | -|----------|-------| -| `applicationId` | `com.streamplayer` | -| `minSdk` | 21 | -| `targetSdk` | 33 | -| `versionCode` | 90000 | -| `versionName` | "9.0.0" | -| `compileSdk` | 33 | - -## 🔐 Permisos y Seguridad - -La aplicación requiere los siguientes permisos: -- ✅ `INTERNET` - Para streaming de contenido -- ✅ `ACCESS_NETWORK_STATE` - Para verificar conectividad -- ✅ `CHANGE_NETWORK_STATE` - Para optimización de red - -## 🐛 Troubleshooting - -### Problemas Comunes - -**Error de Conexión** -- Verificar conexión a internet -- Confirmar configuración DNS -- Revisar disponibilidad del servicio de streaming - -**Error de Reproducción** -- Validar formato de URL -- Verificar permisos de red -- Revisar logs de ExoPlayer - -**Build Fail** -```bash -# Limpiar proyecto -./gradlew clean - -# Rebuild -./gradlew build -``` - -## 📝 Logs y Debug - -La aplicación incluye console logging para: -- Configuración DNS -- Resolución de URLs -- Estados del reproductor -- Errores de red - -## 🤝 Contribución - -1. Fork del repositorio -2. Feature branch (`git checkout -b feature/NuevaCaracteristica`) -3. Commit cambios (`git commit -m 'Add feature'`) -4. Push al branch (`git push origin feature/NuevaCaracteristica`) -5. Pull Request - -## 📄 Licencia - -Este proyecto está licenciado bajo la Licencia MIT - ver archivo [LICENSE](LICENSE) para detalles. - -## 👨‍💻 Autor - -**renato97** - [Gitea Profile](https://gitea.cbcren.online/renato97) - ---- - -⚠️ **Disclaimer**: Esta aplicación es para fines educativos y de demostración. El usuario es responsable de cumplir con los términos de servicio de las plataformas de streaming utilizadas. - -## 📞 Soporte - -Para soporte y preguntas: -- 📧 Crear un issue en el repositorio -- 💬 Comentarios en el código -- 📱 Testing en dispositivos reales recomendado - ---- - -**🔗 Repositorio**: https://gitea.cbcren.online/renato97/app +Este branch **no** incluye ningún archivo Android; es solo el código fuente de la versión Windows. Para la versión móvil sigue usando `main`. diff --git a/StreamPlayer.apk b/StreamPlayer.apk deleted file mode 100644 index 08248f2..0000000 Binary files a/StreamPlayer.apk and /dev/null differ diff --git a/app/.idea/.gitignore b/app/.idea/.gitignore deleted file mode 100644 index eaf91e2..0000000 --- a/app/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/app/.idea/AndroidProjectSystem.xml b/app/.idea/AndroidProjectSystem.xml deleted file mode 100644 index d58d49b..0000000 --- a/app/.idea/AndroidProjectSystem.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/.idea/caches/deviceStreaming.xml b/app/.idea/caches/deviceStreaming.xml deleted file mode 100644 index f731c33..0000000 --- a/app/.idea/caches/deviceStreaming.xml +++ /dev/null @@ -1,1017 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/.idea/deviceManager.xml b/app/.idea/deviceManager.xml deleted file mode 100644 index 81c3e56..0000000 --- a/app/.idea/deviceManager.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/.idea/gradle.xml b/app/.idea/gradle.xml deleted file mode 100644 index 334eea7..0000000 --- a/app/.idea/gradle.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/.idea/migrations.xml b/app/.idea/migrations.xml deleted file mode 100644 index 48052b2..0000000 --- a/app/.idea/migrations.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/.idea/misc.xml b/app/.idea/misc.xml deleted file mode 100644 index 143c6a4..0000000 --- a/app/.idea/misc.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/.idea/runConfigurations.xml b/app/.idea/runConfigurations.xml deleted file mode 100644 index 5bd6771..0000000 --- a/app/.idea/runConfigurations.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index 5441d65..0000000 --- a/app/build.gradle +++ /dev/null @@ -1,60 +0,0 @@ -apply plugin: 'com.android.application' - -android { - namespace "com.streamplayer" - compileSdk 33 - - defaultConfig { - applicationId "com.streamplayer" - minSdk 21 - targetSdk 33 - versionCode 94600 - versionName "9.4.6" - buildConfigField "String", "DEVICE_REGISTRY_URL", '"http://194.163.191.200:4000"' - } - - buildTypes { - release { - minifyEnabled false - signingConfig signingConfigs.debug - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - lint { - checkReleaseBuilds = false - abortOnError = false - } - - buildFeatures { - buildConfig = true - } - - packaging { - resources { - excludes += 'META-INF/com.android.tools/proguard/coroutines.pro' - } - } -} - -dependencies { - implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.recyclerview:recyclerview:1.3.2' - - // ExoPlayer para reproducción de video - implementation 'com.google.android.exoplayer:exoplayer:2.18.7' - implementation 'com.google.android.exoplayer:extension-okhttp:2.18.7' - implementation 'com.squareup.okhttp3:okhttp:4.12.0' - implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.12.0' - - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' -} diff --git a/app/build_simple.gradle b/app/build_simple.gradle deleted file mode 100644 index 7f9f732..0000000 --- a/app/build_simple.gradle +++ /dev/null @@ -1,31 +0,0 @@ -apply plugin: 'com.android.application' - -android { - compileSdkVersion 28 - buildToolsVersion "28.0.3" - - defaultConfig { - applicationId "com.streamplayer" - minSdkVersion 21 - targetSdkVersion 28 - versionCode 1 - versionName "1.0" - } - - buildTypes { - release { - minifyEnabled false - } - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } -} - -dependencies { - implementation 'com.google.android.exoplayer:exoplayer:2.8.4' - implementation 'androidx.appcompat:appcompat:1.0.0' - implementation 'androidx.constraintlayout:constraintlayout:1.1.2' -} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro deleted file mode 100644 index 44f8fa8..0000000 --- a/app/proguard-rules.pro +++ /dev/null @@ -1,24 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile - --keep class com.google.android.exoplayer2.** { *; } --keep class com.streamplayer.** { *; } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml deleted file mode 100644 index 606252c..0000000 --- a/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/java/com/streamplayer/ChannelAdapter.java b/app/src/main/java/com/streamplayer/ChannelAdapter.java deleted file mode 100644 index cc76548..0000000 --- a/app/src/main/java/com/streamplayer/ChannelAdapter.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.streamplayer; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import java.util.ArrayList; -import java.util.List; - -public class ChannelAdapter extends RecyclerView.Adapter { - - public interface OnChannelClickListener { - void onChannelClick(StreamChannel channel); - } - - private final List channels = new ArrayList<>(); - private final OnChannelClickListener listener; - - public ChannelAdapter(OnChannelClickListener listener) { - this.listener = listener; - } - - @NonNull - @Override - public ChannelViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_channel, parent, false); - return new ChannelViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull ChannelViewHolder holder, int position) { - StreamChannel channel = channels.get(position); - holder.name.setText(channel.getName()); - holder.icon.setImageResource(R.drawable.ic_channel_default); - holder.itemView.setOnClickListener(v -> { - if (listener != null) { - listener.onChannelClick(channel); - } - }); - holder.itemView.setOnFocusChangeListener((v, hasFocus) -> { - float scale = hasFocus ? 1.08f : 1f; - v.animate().scaleX(scale).scaleY(scale).setDuration(120).start(); - v.setSelected(hasFocus); - }); - } - - @Override - public int getItemCount() { - return channels.size(); - } - - static class ChannelViewHolder extends RecyclerView.ViewHolder { - final ImageView icon; - final TextView name; - - ChannelViewHolder(@NonNull View itemView) { - super(itemView); - icon = itemView.findViewById(R.id.channel_icon); - name = itemView.findViewById(R.id.channel_name); - } - } - - public void submitList(List newChannels) { - channels.clear(); - if (newChannels != null) { - channels.addAll(newChannels); - } - notifyDataSetChanged(); - } -} diff --git a/app/src/main/java/com/streamplayer/ChannelRepository.java b/app/src/main/java/com/streamplayer/ChannelRepository.java deleted file mode 100644 index 1a1cdf8..0000000 --- a/app/src/main/java/com/streamplayer/ChannelRepository.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.streamplayer; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -public final class ChannelRepository { - - private static final List CHANNELS = createChannels(); - - private static List createChannels() { - List channels = new ArrayList<>(Arrays.asList( - new StreamChannel("ESPN", "https://streamtpmedia.com/global2.php?stream=espn"), - new StreamChannel("ESPN 2", "https://streamtpmedia.com/global2.php?stream=espn2"), - new StreamChannel("ESPN 3", "https://streamtpmedia.com/global2.php?stream=espn3"), - new StreamChannel("ESPN 4", "https://streamtpmedia.com/global2.php?stream=espn4"), - new StreamChannel("ESPN 3 MX", "https://streamtpmedia.com/global2.php?stream=espn3mx"), - new StreamChannel("ESPN 5", "https://streamtpmedia.com/global2.php?stream=espn5"), - new StreamChannel("Fox Sports 3 MX", "https://streamtpmedia.com/global2.php?stream=foxsports3mx"), - new StreamChannel("ESPN 6", "https://streamtpmedia.com/global2.php?stream=espn6"), - new StreamChannel("Fox Sports MX", "https://streamtpmedia.com/global2.php?stream=foxsportsmx"), - new StreamChannel("ESPN 7", "https://streamtpmedia.com/global2.php?stream=espn7"), - new StreamChannel("Azteca Deportes", "https://streamtpmedia.com/global2.php?stream=azteca_deportes"), - new StreamChannel("Win Plus", "https://streamtpmedia.com/global2.php?stream=winplus"), - new StreamChannel("DAZN 1", "https://streamtpmedia.com/global2.php?stream=dazn1"), - new StreamChannel("Win Plus 2", "https://streamtpmedia.com/global2.php?stream=winplus2"), - new StreamChannel("DAZN 2", "https://streamtpmedia.com/global2.php?stream=dazn2"), - new StreamChannel("Win Sports", "https://streamtpmedia.com/global2.php?stream=winsports"), - new StreamChannel("DAZN LaLiga", "https://streamtpmedia.com/global2.php?stream=dazn_laliga"), - new StreamChannel("Win Plus Online 1", "https://streamtpmedia.com/global2.php?stream=winplusonline1"), - new StreamChannel("Caracol TV", "https://streamtpmedia.com/global2.php?stream=caracoltv"), - new StreamChannel("Fox 1 AR", "https://streamtpmedia.com/global2.php?stream=fox1ar"), - new StreamChannel("Fox 2 USA", "https://streamtpmedia.com/global2.php?stream=fox_2_usa"), - new StreamChannel("Fox 2 AR", "https://streamtpmedia.com/global2.php?stream=fox2ar"), - new StreamChannel("TNT 1 GB", "https://streamtpmedia.com/global2.php?stream=tnt_1_gb"), - new StreamChannel("TNT 2 GB", "https://streamtpmedia.com/global2.php?stream=tnt_2_gb"), - new StreamChannel("Fox 3 AR", "https://streamtpmedia.com/global2.php?stream=fox3ar"), - new StreamChannel("Universo USA", "https://streamtpmedia.com/global2.php?stream=universo_usa"), - new StreamChannel("DSports", "https://streamtpmedia.com/global2.php?stream=dsports"), - new StreamChannel("Univision USA", "https://streamtpmedia.com/global2.php?stream=univision_usa"), - new StreamChannel("DSports 2", "https://streamtpmedia.com/global2.php?stream=dsports2"), - new StreamChannel("Fox Deportes USA", "https://streamtpmedia.com/global2.php?stream=fox_deportes_usa"), - new StreamChannel("DSports Plus", "https://streamtpmedia.com/global2.php?stream=dsportsplus"), - new StreamChannel("Fox Sports 2 MX", "https://streamtpmedia.com/global2.php?stream=foxsports2mx"), - new StreamChannel("TNT Sports Chile", "https://streamtpmedia.com/global2.php?stream=tntsportschile"), - new StreamChannel("Fox Sports Premium", "https://streamtpmedia.com/global2.php?stream=foxsportspremium"), - new StreamChannel("TNT Sports", "https://streamtpmedia.com/global2.php?stream=tntsports"), - new StreamChannel("ESPN MX", "https://streamtpmedia.com/global2.php?stream=espnmx"), - new StreamChannel("ESPN Premium", "https://streamtpmedia.com/global2.php?stream=espnpremium"), - new StreamChannel("ESPN 2 MX", "https://streamtpmedia.com/global2.php?stream=espn2mx"), - new StreamChannel("TyC Sports", "https://streamtpmedia.com/global2.php?stream=tycsports"), - new StreamChannel("TUDN USA", "https://streamtpmedia.com/global2.php?stream=tudn_usa"), - new StreamChannel("Telefe", "https://streamtpmedia.com/global2.php?stream=telefe"), - new StreamChannel("TNT 3 GB", "https://streamtpmedia.com/global2.php?stream=tnt_3_gb"), - new StreamChannel("TV Pública", "https://streamtpmedia.com/global2.php?stream=tv_publica"), - new StreamChannel("Fox 1 USA", "https://streamtpmedia.com/global2.php?stream=fox_1_usa"), - new StreamChannel("Liga 1 Max", "https://streamtpmedia.com/global2.php?stream=liga1max"), - new StreamChannel("Gol TV", "https://streamtpmedia.com/global2.php?stream=goltv"), - new StreamChannel("VTV Plus", "https://streamtpmedia.com/global2.php?stream=vtvplus"), - new StreamChannel("ESPN Deportes", "https://streamtpmedia.com/global2.php?stream=espndeportes"), - new StreamChannel("Gol Perú", "https://streamtpmedia.com/global2.php?stream=golperu"), - new StreamChannel("TNT 4 GB", "https://streamtpmedia.com/global2.php?stream=tnt_4_gb"), - new StreamChannel("SportTV BR 1", "https://streamtpmedia.com/global2.php?stream=sporttvbr1"), - new StreamChannel("SportTV BR 2", "https://streamtpmedia.com/global2.php?stream=sporttvbr2"), - new StreamChannel("SportTV BR 3", "https://streamtpmedia.com/global2.php?stream=sporttvbr3"), - new StreamChannel("Premiere 1", "https://streamtpmedia.com/global2.php?stream=premiere1"), - new StreamChannel("Premiere 2", "https://streamtpmedia.com/global2.php?stream=premiere2"), - new StreamChannel("Premiere 3", "https://streamtpmedia.com/global2.php?stream=premiere3"), - new StreamChannel("ESPN NL 1", "https://streamtpmedia.com/global2.php?stream=espn_nl1"), - new StreamChannel("ESPN NL 2", "https://streamtpmedia.com/global2.php?stream=espn_nl2"), - new StreamChannel("ESPN NL 3", "https://streamtpmedia.com/global2.php?stream=espn_nl3"), - new StreamChannel("Caliente TV MX", "https://streamtpmedia.com/global2.php?stream=calientetvmx"), - new StreamChannel("USA Network", "https://streamtpmedia.com/global2.php?stream=usa_network"), - new StreamChannel("TyC Internacional", "https://streamtpmedia.com/global2.php?stream=tycinternacional"), - new StreamChannel("Canal 5 MX", "https://streamtpmedia.com/global2.php?stream=canal5mx"), - new StreamChannel("TUDN MX", "https://streamtpmedia.com/global2.php?stream=TUDNMX"), - new StreamChannel("FUTV", "https://streamtpmedia.com/global2.php?stream=futv"), - new StreamChannel("LaLiga Hypermotion", "https://streamtpmedia.com/global2.php?stream=laligahypermotion") - )); - channels.sort(Comparator.comparing(StreamChannel::getName, String.CASE_INSENSITIVE_ORDER)); - return Collections.unmodifiableList(channels); - } - - private ChannelRepository() { - } - - public static List getChannels() { - return CHANNELS; - } -} diff --git a/app/src/main/java/com/streamplayer/DNSSetter.java b/app/src/main/java/com/streamplayer/DNSSetter.java deleted file mode 100644 index ea6987f..0000000 --- a/app/src/main/java/com/streamplayer/DNSSetter.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.streamplayer; - -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.Network; -import android.net.NetworkCapabilities; -import android.net.NetworkRequest; -import android.os.Build; - -import java.net.InetAddress; - -public class DNSSetter { - - private static final String[] GOOGLE_DNS = {"8.8.8.8", "8.8.4.4"}; - - public static void configureDNSToGoogle(Context context) { - try { - // Configurar propiedades del sistema para usar DNS específicos - System.setProperty("networkaddress.cache.ttl", "60"); - System.setProperty("networkaddress.cache.negative.ttl", "10"); - - // Forzar resolución usando DNS de Google - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - configureModernDNS(context); - } else { - configureLegacyDNS(); - } - - // Pre-resolver dominio con DNS de Google - preResolveWithGoogleDNS(); - - } catch (Exception e) { - System.out.println("Error configurando DNS de Google: " + e.getMessage()); - } - } - - private static void configureModernDNS(Context context) { - try { - ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - - NetworkRequest networkRequest = new NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .build(); - - connectivityManager.registerNetworkCallback(networkRequest, new ConnectivityManager.NetworkCallback() { - @Override - public void onAvailable(Network network) { - super.onAvailable(network); - - // Configuración para priorizar DNS de Google - // Aunque no podemos cambiar DNS directamente sin permisos especiales, - // podemos optimizar la configuración de red - try { - NetworkCapabilities caps = connectivityManager.getNetworkCapabilities(network); - if (caps != null) { - System.out.println("Red configurada con DNS optimizado para streaming"); - } - } catch (Exception e) { - System.out.println("Error en configuración de red: " + e.getMessage()); - } - } - }); - - } catch (Exception e) { - System.out.println("Error configurando DNS moderno: " + e.getMessage()); - } - } - - private static void configureLegacyDNS() { - try { - // Para versiones antiguas, configuramos propiedades del sistema - System.setProperty("sun.net.inetaddr.ttl", "60"); - System.setProperty("sun.net.inetaddr.negative.ttl", "10"); - - System.out.println("DNS legacy configurado para streaming"); - } catch (Exception e) { - System.out.println("Error configurando DNS legacy: " + e.getMessage()); - } - } - - private static void preResolveWithGoogleDNS() { - try { - // Pre-resolver algunos dominios comunes para caching - Thread thread = new Thread(() -> { - try { - String[] domains = {"streamtpmedia.com", "google.com", "doubleclick.net"}; - for (String domain : domains) { - try { - InetAddress.getByName(domain); - System.out.println("Pre-resuelto: " + domain); - } catch (Exception e) { - System.out.println("Error pre-resolviendo " + domain + ": " + e.getMessage()); - } - } - } catch (Exception e) { - System.out.println("Error en pre-resolución: " + e.getMessage()); - } - }); - thread.start(); - - } catch (Exception e) { - System.out.println("Error en pre-resolución DNS: " + e.getMessage()); - } - } - - public static String getGoogleDNSInfo() { - return "DNS de Google configurado: " + String.join(", ", GOOGLE_DNS); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/streamplayer/DeviceRegistry.java b/app/src/main/java/com/streamplayer/DeviceRegistry.java deleted file mode 100644 index bf286ed..0000000 --- a/app/src/main/java/com/streamplayer/DeviceRegistry.java +++ /dev/null @@ -1,164 +0,0 @@ -package com.streamplayer; - -import android.content.Context; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import android.provider.Settings; -import android.text.TextUtils; -import android.util.Log; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; -import java.util.Locale; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; - -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; - -/** - * Informa al dashboard qué dispositivos tienen instalada la app y permite bloquearlos remotamente. - */ -public class DeviceRegistry { - - public interface Callback { - void onAllowed(); - - void onBlocked(String reason, String tokenPart); - - void onError(String message); - } - - private static final String TAG = "DeviceRegistry"; - private static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); - - private final Context appContext; - private final OkHttpClient httpClient; - private final ExecutorService executorService; - private final Handler mainHandler = new Handler(Looper.getMainLooper()); - - public DeviceRegistry(Context context) { - this.appContext = context.getApplicationContext(); - this.httpClient = new OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(15, TimeUnit.SECONDS) - .callTimeout(20, TimeUnit.SECONDS) - .build(); - this.executorService = Executors.newSingleThreadExecutor(); - } - - public void syncDevice(Callback callback) { - if (TextUtils.isEmpty(BuildConfig.DEVICE_REGISTRY_URL)) { - postAllowed(callback); - return; - } - executorService.execute(() -> { - try { - JSONObject payload = new JSONObject(); - payload.put("deviceId", getDeviceId()); - payload.put("deviceName", Build.MODEL); - payload.put("model", Build.MODEL); - payload.put("manufacturer", capitalize(Build.MANUFACTURER)); - payload.put("osVersion", Build.VERSION.RELEASE + " (API " + Build.VERSION.SDK_INT + ")"); - payload.put("appVersionName", BuildConfig.VERSION_NAME); - payload.put("appVersionCode", BuildConfig.VERSION_CODE); - - String endpoint = sanitizeBaseUrl(BuildConfig.DEVICE_REGISTRY_URL) + "/api/devices/register"; - RequestBody body = RequestBody.create(payload.toString(), JSON); - Request request = new Request.Builder() - .url(endpoint) - .post(body) - .build(); - try (Response response = httpClient.newCall(request).execute()) { - if (!response.isSuccessful() || response.body() == null) { - throw new IOException("HTTP " + response.code()); - } - String responseText = response.body().string(); - JSONObject json = new JSONObject(responseText); - JSONObject deviceJson = json.optJSONObject("device"); - JSONObject verificationJson = json.optJSONObject("verification"); - boolean blocked = json.optBoolean("blocked", false); - String reason = json.optString("message"); - if (TextUtils.isEmpty(reason) && deviceJson != null) { - reason = deviceJson.optString("notes", ""); - } - String tokenPart = ""; - if (verificationJson != null) { - boolean verificationRequired = verificationJson.optBoolean("required", false); - blocked = blocked || verificationRequired; - tokenPart = verificationJson.optString("clientTokenPart", ""); - } - if (blocked) { - postBlocked(callback, reason, tokenPart); - } else { - postAllowed(callback); - } - } - } catch (IOException | JSONException e) { - Log.w(TAG, "Device sync error", e); - postError(callback, e.getMessage()); - } - }); - } - - private String sanitizeBaseUrl(String base) { - if (TextUtils.isEmpty(base)) { - return ""; - } - if (base.endsWith("/")) { - return base.substring(0, base.length() - 1); - } - return base; - } - - private String getDeviceId() { - String id = Settings.Secure.getString(appContext.getContentResolver(), - Settings.Secure.ANDROID_ID); - if (TextUtils.isEmpty(id)) { - id = Build.MODEL + "-" + Build.BOARD + "-" + BuildConfig.VERSION_CODE; - } - return id; - } - - private String capitalize(String value) { - if (TextUtils.isEmpty(value)) { - return ""; - } - return value.substring(0, 1).toUpperCase(Locale.getDefault()) - + value.substring(1); - } - - public void release() { - executorService.shutdownNow(); - } - - private void postAllowed(Callback callback) { - if (callback == null) { - return; - } - mainHandler.post(callback::onAllowed); - } - - private void postBlocked(Callback callback, String reason, String tokenPart) { - if (callback == null) { - return; - } - String reasonText = reason == null ? "" : reason; - String token = tokenPart == null ? "" : tokenPart; - mainHandler.post(() -> callback.onBlocked(reasonText, token)); - } - - private void postError(Callback callback, String message) { - if (callback == null) { - return; - } - mainHandler.post(() -> callback.onError(message)); - } -} diff --git a/app/src/main/java/com/streamplayer/EventAdapter.java b/app/src/main/java/com/streamplayer/EventAdapter.java deleted file mode 100644 index 845351d..0000000 --- a/app/src/main/java/com/streamplayer/EventAdapter.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.streamplayer; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -public class EventAdapter extends RecyclerView.Adapter { - - public interface OnEventClickListener { - void onEventClick(EventItem event); - } - - private final List events = new ArrayList<>(); - private final OnEventClickListener listener; - - public EventAdapter(OnEventClickListener listener) { - this.listener = listener; - } - - public void submitList(List newEvents) { - events.clear(); - events.addAll(newEvents); - notifyDataSetChanged(); - } - - @NonNull - @Override - public EventViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_event, parent, false); - return new EventViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull EventViewHolder holder, int position) { - EventItem event = events.get(position); - holder.title.setText(event.getTitle()); - holder.time.setText(event.getTime()); - holder.channel.setText(event.getChannelName()); - holder.status.setText(buildStatusText(event)); - holder.itemView.setOnClickListener(v -> { - if (listener != null) { - listener.onEventClick(event); - } - }); - } - - @Override - public int getItemCount() { - return events.size(); - } - - static class EventViewHolder extends RecyclerView.ViewHolder { - final TextView title; - final TextView time; - final TextView channel; - final TextView status; - - EventViewHolder(@NonNull View itemView) { - super(itemView); - title = itemView.findViewById(R.id.event_title); - time = itemView.findViewById(R.id.event_time); - channel = itemView.findViewById(R.id.event_channel); - status = itemView.findViewById(R.id.event_status); - } - } - - private String buildStatusText(EventItem event) { - long start = event.getStartMillis(); - if (start <= 0) { - return event.getStatus(); - } - long now = System.currentTimeMillis(); - long diff = start - now; - if (diff > 0) { - long hours = diff / 3600000; - long minutes = (diff % 3600000) / 60000; - if (hours > 0) { - return String.format(Locale.getDefault(), "En %dh %02dm", hours, minutes); - } else { - return String.format(Locale.getDefault(), "En %d min", Math.max(1, minutes)); - } - } else if (Math.abs(diff) <= 2 * 3600000L) { - return "En vivo"; - } else { - return "Finalizado"; - } - } -} diff --git a/app/src/main/java/com/streamplayer/EventItem.java b/app/src/main/java/com/streamplayer/EventItem.java deleted file mode 100644 index f2127ee..0000000 --- a/app/src/main/java/com/streamplayer/EventItem.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.streamplayer; - -public class EventItem { - private final String title; - private final String time; - private final String category; - private final String status; - private final String pageUrl; - private final String channelName; - private final long startMillis; - - public EventItem(String title, String time, String category, String status, String pageUrl, String channelName, long startMillis) { - this.title = title; - this.time = time; - this.category = category; - this.status = status; - this.pageUrl = pageUrl; - this.channelName = channelName; - this.startMillis = startMillis; - } - - public String getTitle() { - return title; - } - - public String getTime() { - return time; - } - - public String getCategory() { - return category; - } - - public String getStatus() { - return status; - } - - public String getPageUrl() { - return pageUrl; - } - - public String getChannelName() { - return channelName; - } - - public long getStartMillis() { - return startMillis; - } -} diff --git a/app/src/main/java/com/streamplayer/EventRepository.java b/app/src/main/java/com/streamplayer/EventRepository.java deleted file mode 100644 index e4eb294..0000000 --- a/app/src/main/java/com/streamplayer/EventRepository.java +++ /dev/null @@ -1,163 +0,0 @@ -package com.streamplayer; - -import android.content.Context; -import android.content.SharedPreferences; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class EventRepository { - - private static final String PREFS_NAME = "events_cache"; - private static final String KEY_JSON = "json"; - private static final String KEY_TIMESTAMP = "timestamp"; - private static final long CACHE_DURATION = 24L * 60 * 60 * 1000; // 24 horas - private static final String EVENTS_URL = "https://streamtpmedia.com/eventos.json"; - - public interface Callback { - void onSuccess(List events); - - void onError(String message); - } - - public void loadEvents(Context context, boolean forceRefresh, Callback callback) { - SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - long last = prefs.getLong(KEY_TIMESTAMP, 0); - long now = System.currentTimeMillis(); - if (!forceRefresh && now - last < CACHE_DURATION) { - String cachedJson = prefs.getString(KEY_JSON, null); - if (cachedJson != null) { - try { - callback.onSuccess(parseEvents(cachedJson)); - return; - } catch (JSONException ignored) { - } - } - } - - new Thread(() -> { - try { - String json = downloadJson(); - List events = parseEvents(json); - prefs.edit().putString(KEY_JSON, json).putLong(KEY_TIMESTAMP, System.currentTimeMillis()).apply(); - callback.onSuccess(events); - } catch (IOException | JSONException e) { - String cachedJson = prefs.getString(KEY_JSON, null); - if (cachedJson != null) { - try { - callback.onSuccess(parseEvents(cachedJson)); - return; - } catch (JSONException ignored) { - } - } - callback.onError(e.getMessage() != null ? e.getMessage() : "Error desconocido"); - } - }).start(); - } - - public void prefetchEvents(Context context) { - SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - new Thread(() -> { - try { - String json = downloadJson(); - parseEvents(json); - prefs.edit() - .putString(KEY_JSON, json) - .putLong(KEY_TIMESTAMP, System.currentTimeMillis()) - .apply(); - } catch (IOException | JSONException ignored) { - } - }).start(); - } - - private String downloadJson() throws IOException { - URL url = new URL(EVENTS_URL); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setConnectTimeout(15000); - connection.setReadTimeout(15000); - connection.setRequestMethod("GET"); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) { - StringBuilder builder = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - builder.append(line); - } - return builder.toString(); - } finally { - connection.disconnect(); - } - } - - private List parseEvents(String json) throws JSONException { - JSONArray array = new JSONArray(json); - List events = new ArrayList<>(); - for (int i = 0; i < array.length(); i++) { - JSONObject obj = array.getJSONObject(i); - String title = obj.optString("title"); - String time = obj.optString("time"); - String category = obj.optString("category"); - String status = obj.optString("status"); - String link = obj.optString("link"); - String normalized = normalizeLink(link); - long startMillis = parseEventTime(time); - events.add(new EventItem(title, time, category, status, normalized, extractChannelName(link), startMillis)); - } - return Collections.unmodifiableList(events); - } - - private String normalizeLink(String link) { - if (link == null) { - return ""; - } - return link.replace("global1.php", "global2.php"); - } - - private String extractChannelName(String link) { - if (link == null) { - return ""; - } - int idx = link.indexOf("stream="); - if (idx == -1) { - return ""; - } - return link.substring(idx + 7).replace("_", " ").toUpperCase(); - } - - private long parseEventTime(String time) { - if (time == null || time.isEmpty()) { - return -1; - } - try { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm"); - LocalTime localTime = LocalTime.parse(time.trim(), formatter); - ZoneId zone = ZoneId.of("America/Argentina/Buenos_Aires"); - LocalDate today = LocalDate.now(zone); - ZonedDateTime start = ZonedDateTime.of(LocalDateTime.of(today, localTime), zone); - ZonedDateTime now = ZonedDateTime.now(zone); - if (start.isBefore(now.minusHours(12))) { - start = start.plusDays(1); - } - return start.toInstant().toEpochMilli(); - } catch (DateTimeParseException e) { - return -1; - } - } -} diff --git a/app/src/main/java/com/streamplayer/MainActivity.java b/app/src/main/java/com/streamplayer/MainActivity.java deleted file mode 100644 index b36d16c..0000000 --- a/app/src/main/java/com/streamplayer/MainActivity.java +++ /dev/null @@ -1,546 +0,0 @@ -package com.streamplayer; - -import android.app.AlertDialog; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.text.TextUtils; -import android.graphics.drawable.ColorDrawable; -import android.view.View; -import android.widget.Button; -import android.widget.ProgressBar; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.appcompat.app.AppCompatActivity; -import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -public class MainActivity extends AppCompatActivity { - - private static final long EVENT_PREFETCH_INTERVAL_MS = TimeUnit.HOURS.toMillis(1); - - private RecyclerView sectionList; - private RecyclerView contentList; - private ProgressBar loadingIndicator; - private TextView messageView; - private TextView contentTitle; - private Button eventsRefreshButton; - - private ChannelAdapter channelAdapter; - private EventAdapter eventAdapter; - private EventRepository eventRepository; - private SectionAdapter sectionAdapter; - private GridLayoutManager channelLayoutManager; - private LinearLayoutManager eventLayoutManager; - private final List cachedEvents = new ArrayList<>(); - private List sections; - private SectionEntry currentSection; - private UpdateManager updateManager; - private AlertDialog updateDialog; - private AlertDialog blockedDialog; - private DeviceRegistry deviceRegistry; - private Handler eventPrefetchHandler; - private Runnable eventPrefetchRunnable; - private boolean isEventsRefreshing; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - sectionList = findViewById(R.id.section_list); - contentList = findViewById(R.id.content_list); - loadingIndicator = findViewById(R.id.loading_indicator); - messageView = findViewById(R.id.message_view); - contentTitle = findViewById(R.id.content_title); - eventsRefreshButton = findViewById(R.id.events_refresh_button); - eventsRefreshButton.setOnClickListener(v -> manualRefreshEvents()); - applyFocusHighlight(eventsRefreshButton); - - channelAdapter = new ChannelAdapter( - channel -> openPlayer(channel.getName(), channel.getPageUrl())); - eventAdapter = new EventAdapter(event -> openPlayer(event.getTitle(), event.getPageUrl())); - eventRepository = new EventRepository(); - channelLayoutManager = new GridLayoutManager(this, getSpanCount()); - eventLayoutManager = new LinearLayoutManager(this); - - sections = buildSections(); - sectionList.setLayoutManager(new LinearLayoutManager(this)); - sectionAdapter = new SectionAdapter(getSectionTitles(), this::selectSection); - sectionList.setAdapter(sectionAdapter); - - selectSection(0); - - updateManager = new UpdateManager(this); - updateManager.checkForUpdates(new UpdateManager.UpdateCallback() { - @Override - public void onUpdateAvailable(UpdateManager.UpdateInfo info) { - handleUpdateInfo(info); - } - - @Override - public void onUpToDate() { - // Nothing to do. - } - - @Override - public void onError(String message) { - Toast.makeText(MainActivity.this, - getString(R.string.update_error_checking, message), - Toast.LENGTH_SHORT).show(); - } - }); - - deviceRegistry = new DeviceRegistry(this); - deviceRegistry.syncDevice(new DeviceRegistry.Callback() { - @Override - public void onAllowed() { - // Device authorized, continue normally. - } - - @Override - public void onBlocked(String reason, String tokenPart) { - showBlockedDialog(reason, tokenPart); - } - - @Override - public void onError(String message) { - if (!TextUtils.isEmpty(message)) { - Toast.makeText(MainActivity.this, - getString(R.string.device_registry_error, message), - Toast.LENGTH_SHORT).show(); - } - } - }); - - startEventPrefetchScheduler(); - } - - @Override - protected void onResume() { - super.onResume(); - if (updateManager != null) { - updateManager.resumePendingInstall(this); - } - } - - @Override - protected void onDestroy() { - super.onDestroy(); - if (updateDialog != null && updateDialog.isShowing()) { - updateDialog.dismiss(); - } - if (blockedDialog != null && blockedDialog.isShowing()) { - blockedDialog.dismiss(); - } - if (updateManager != null) { - updateManager.release(); - } - if (deviceRegistry != null) { - deviceRegistry.release(); - } - stopEventPrefetchScheduler(); - } - - @Override - public void onBackPressed() { - closeAppCompletely(); - } - - private void selectSection(int index) { - if (sections == null || sections.isEmpty()) { - return; - } - if (index < 0 || index >= sections.size()) { - index = 0; - } - sectionAdapter.setSelectedIndex(index); - currentSection = sections.get(index); - if (currentSection.type == SectionEntry.Type.EVENTS) { - showEvents(); - } else { - showChannels(currentSection); - } - } - - private void showChannels(SectionEntry section) { - updateEventsRefreshVisibility(false); - contentTitle.setText(section.title); - contentList.setLayoutManager(channelLayoutManager); - contentList.setAdapter(channelAdapter); - loadingIndicator.setVisibility(View.GONE); - channelAdapter.submitList(section.channels); - if (section.channels.isEmpty()) { - messageView.setVisibility(View.VISIBLE); - messageView.setText(R.string.message_no_channels); - } else { - messageView.setVisibility(View.GONE); - contentList.post(() -> contentList.scrollToPosition(0)); - } - } - - private void showEvents() { - updateEventsRefreshVisibility(true); - contentTitle.setText(currentSection != null ? currentSection.title : getString(R.string.section_events)); - contentList.setLayoutManager(eventLayoutManager); - contentList.setAdapter(eventAdapter); - if (cachedEvents.isEmpty()) { - loadEvents(false); - } else { - displayEvents(); - } - } - - private void loadEvents(boolean forceRefresh) { - if (currentSection != null && currentSection.type == SectionEntry.Type.EVENTS) { - setEventsRefreshing(true); - } - loadingIndicator.setVisibility(View.VISIBLE); - messageView.setVisibility(View.GONE); - eventRepository.loadEvents(this, forceRefresh, new EventRepository.Callback() { - @Override - public void onSuccess(List events) { - runOnUiThread(() -> { - cachedEvents.clear(); - cachedEvents.addAll(events); - if (currentSection != null && currentSection.type == SectionEntry.Type.EVENTS) { - displayEvents(); - } else { - loadingIndicator.setVisibility(View.GONE); - } - setEventsRefreshing(false); - }); - } - - @Override - public void onError(String message) { - runOnUiThread(() -> { - loadingIndicator.setVisibility(View.GONE); - messageView.setVisibility(View.VISIBLE); - messageView.setText(getString(R.string.message_events_error, message)); - setEventsRefreshing(false); - }); - } - }); - } - - private void manualRefreshEvents() { - if (isEventsRefreshing) { - return; - } - if (currentSection == null || currentSection.type != SectionEntry.Type.EVENTS) { - return; - } - loadEvents(true); - } - - private void updateEventsRefreshVisibility(boolean visible) { - if (eventsRefreshButton == null) { - return; - } - eventsRefreshButton.setVisibility(visible ? View.VISIBLE : View.GONE); - if (visible) { - eventsRefreshButton.setEnabled(!isEventsRefreshing); - eventsRefreshButton.setText(isEventsRefreshing - ? getString(R.string.events_refreshing) - : getString(R.string.events_refresh_action)); - } - } - - private void setEventsRefreshing(boolean refreshing) { - isEventsRefreshing = refreshing; - if (eventsRefreshButton == null) { - return; - } - if (eventsRefreshButton.getVisibility() == View.VISIBLE) { - eventsRefreshButton.setEnabled(!refreshing); - eventsRefreshButton.setText(refreshing - ? getString(R.string.events_refreshing) - : getString(R.string.events_refresh_action)); - } - } - - private void displayEvents() { - loadingIndicator.setVisibility(View.GONE); - if (cachedEvents.isEmpty()) { - messageView.setVisibility(View.VISIBLE); - messageView.setText(R.string.message_no_events); - eventAdapter.submitList(new ArrayList<>()); - } else { - messageView.setVisibility(View.GONE); - eventAdapter.submitList(new ArrayList<>(cachedEvents)); - } - } - - private void openPlayer(String name, String pageUrl) { - Intent intent = new Intent(MainActivity.this, PlayerActivity.class); - intent.putExtra(PlayerActivity.EXTRA_CHANNEL_NAME, name); - intent.putExtra(PlayerActivity.EXTRA_CHANNEL_URL, pageUrl); - startActivity(intent); - } - - private void handleUpdateInfo(UpdateManager.UpdateInfo info) { - if (info == null) { - return; - } - showUpdateDialog(info); - } - - private void showUpdateDialog(UpdateManager.UpdateInfo info) { - if (isFinishing()) { - return; - } - if (updateDialog != null && updateDialog.isShowing()) { - updateDialog.dismiss(); - } - View dialogView = getLayoutInflater().inflate(R.layout.dialog_update, null); - TextView titleView = dialogView.findViewById(R.id.update_title); - TextView messageView = dialogView.findViewById(R.id.update_message); - Button positiveButton = dialogView.findViewById(R.id.update_positive_button); - Button negativeButton = dialogView.findViewById(R.id.update_negative_button); - titleView.setText(R.string.update_required_title); - messageView.setText(buildUpdateMessage(info)); - AlertDialog dialog = new AlertDialog.Builder(this, R.style.ThemeOverlay_StreamPlayer_AlertDialog) - .setView(dialogView) - .setCancelable(false) - .create(); - dialog.setCanceledOnTouchOutside(false); - dialog.show(); - if (dialog.getWindow() != null) { - dialog.getWindow().setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT)); - } - positiveButton.setOnClickListener(v -> { - dialog.dismiss(); - updateManager.downloadUpdate(MainActivity.this, info); - }); - negativeButton.setOnClickListener(v -> closeAppCompletely()); - applyFocusHighlight(positiveButton); - applyFocusHighlight(negativeButton); - positiveButton.requestFocus(); - updateDialog = dialog; - } - - private CharSequence buildUpdateMessage(UpdateManager.UpdateInfo info) { - StringBuilder builder = new StringBuilder(); - builder.append(getString(R.string.update_current_version, - BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)); - builder.append('\n'); - builder.append(getString(R.string.update_latest_version, - info.versionName, info.versionCode)); - if (info.minSupportedVersionCode > 0) { - builder.append('\n').append(getString(R.string.update_min_supported, - info.minSupportedVersionCode)); - } - String size = info.formatSize(this); - if (!size.isEmpty()) { - builder.append('\n').append(getString(R.string.update_download_size, size)); - } - if (info.downloadCount > 0) { - builder.append('\n').append(getString(R.string.update_downloads, - info.downloadCount)); - } - if (!info.releaseNotes.isEmpty()) { - builder.append("\n\n"); - builder.append(getString(R.string.update_release_notes_title)); - builder.append('\n'); - builder.append(info.getReleaseNotesPreview()); - } - return builder.toString(); - } - - private void showBlockedDialog(String reason, String tokenPart) { - if (isFinishing()) { - return; - } - String finalReason = TextUtils.isEmpty(reason) - ? getString(R.string.device_blocked_default_reason) - : reason; - if (blockedDialog != null && blockedDialog.isShowing()) { - blockedDialog.dismiss(); - } - View dialogView = getLayoutInflater().inflate(R.layout.dialog_blocked, null); - TextView messageText = dialogView.findViewById(R.id.blocked_message_text); - View tokenContainer = dialogView.findViewById(R.id.blocked_token_container); - TextView tokenValue = dialogView.findViewById(R.id.blocked_token_value); - messageText.setText(getString(R.string.device_blocked_message, finalReason)); - boolean hasToken = !TextUtils.isEmpty(tokenPart); - if (hasToken) { - tokenContainer.setVisibility(View.VISIBLE); - tokenValue.setText(tokenPart); - tokenValue.setOnClickListener(v -> copyTokenToClipboard(tokenPart)); - } else { - tokenContainer.setVisibility(View.GONE); - } - AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.ThemeOverlay_StreamPlayer_AlertDialog) - .setTitle(R.string.device_blocked_title) - .setView(dialogView) - .setCancelable(false) - .setPositiveButton(R.string.device_blocked_close, - (dialog, which) -> closeAppCompletely()); - if (hasToken) { - builder.setNeutralButton(R.string.device_blocked_copy_token, - (dialog, which) -> copyTokenToClipboard(tokenPart)); - } - blockedDialog = builder.create(); - blockedDialog.show(); - } - - private void copyTokenToClipboard(String tokenPart) { - ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); - if (clipboard == null) { - Toast.makeText(this, R.string.device_blocked_copy_error, Toast.LENGTH_SHORT).show(); - return; - } - ClipData data = ClipData.newPlainText("token", tokenPart); - clipboard.setPrimaryClip(data); - Toast.makeText(this, R.string.device_blocked_copy_success, Toast.LENGTH_SHORT).show(); - } - - private void applyFocusHighlight(View view) { - if (view == null) { - return; - } - view.setOnFocusChangeListener((v, hasFocus) -> { - float scale = hasFocus ? 1.05f : 1f; - v.animate().scaleX(scale).scaleY(scale).setDuration(120).start(); - }); - } - - private void startEventPrefetchScheduler() { - if (eventPrefetchHandler == null) { - eventPrefetchHandler = new Handler(Looper.getMainLooper()); - } - if (eventPrefetchRunnable == null) { - eventPrefetchRunnable = new Runnable() { - @Override - public void run() { - if (eventRepository != null) { - eventRepository.prefetchEvents(getApplicationContext()); - } - if (eventPrefetchHandler != null) { - eventPrefetchHandler.postDelayed(this, EVENT_PREFETCH_INTERVAL_MS); - } - } - }; - } else { - eventPrefetchHandler.removeCallbacks(eventPrefetchRunnable); - } - eventPrefetchHandler.post(eventPrefetchRunnable); - } - - private void stopEventPrefetchScheduler() { - if (eventPrefetchHandler != null && eventPrefetchRunnable != null) { - eventPrefetchHandler.removeCallbacks(eventPrefetchRunnable); - } - } - - private void closeAppCompletely() { - finishAffinity(); - android.os.Process.killProcess(android.os.Process.myPid()); - System.exit(0); - } - - private int getSpanCount() { - return getResources().getInteger(R.integer.channel_grid_span); - } - - private List buildSections() { - List list = new ArrayList<>(); - list.add(SectionEntry.events(getString(R.string.section_events))); - - Map> grouped = new HashMap<>(); - List allChannels = ChannelRepository.getChannels(); - for (StreamChannel channel : allChannels) { - String key = deriveGroupName(channel.getName()); - grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(channel); - } - - List espnChannels = grouped.remove("ESPN"); - if (espnChannels != null && !espnChannels.isEmpty()) { - list.add(SectionEntry.channels("ESPN", espnChannels)); - } - - List remaining = new ArrayList<>(grouped.keySet()); - Collections.sort(remaining, String.CASE_INSENSITIVE_ORDER); - for (String key : remaining) { - List channels = grouped.get(key); - if (channels == null || channels.isEmpty()) { - continue; - } - list.add(SectionEntry.channels(key, channels)); - } - - list.add(SectionEntry.channels(getString(R.string.section_all_channels), allChannels)); - return list; - } - - private List getSectionTitles() { - List titles = new ArrayList<>(); - for (SectionEntry entry : sections) { - titles.add(entry.title); - } - return titles; - } - - private String deriveGroupName(String name) { - if (name == null) { - return getString(R.string.section_all_channels); - } - String upper = name.toUpperCase(Locale.US); - if (upper.startsWith("ESPN")) { - return "ESPN"; - } else if (upper.contains("FOX SPORTS")) { - return "Fox Sports"; - } else if (upper.contains("FOX")) { - return "Fox"; - } else if (upper.contains("TNT")) { - return "TNT"; - } else if (upper.contains("DAZN")) { - return "DAZN"; - } else if (upper.contains("TUDN")) { - return "TUDN"; - } else if (upper.contains("TYC")) { - return "TyC"; - } else if (upper.contains("GOL")) { - return "Gol"; - } - int spaceIndex = upper.indexOf(' '); - return spaceIndex > 0 ? upper.substring(0, spaceIndex) : upper; - } - - private static class SectionEntry { - enum Type { EVENTS, CHANNELS } - - final String title; - final Type type; - final List channels; - - private SectionEntry(String title, Type type, List channels) { - this.title = title; - this.type = type; - this.channels = channels == null ? new ArrayList<>() : new ArrayList<>(channels); - } - - static SectionEntry events(String title) { - return new SectionEntry(title, Type.EVENTS, null); - } - - static SectionEntry channels(String title, List channels) { - return new SectionEntry(title, Type.CHANNELS, channels); - } - } -} diff --git a/app/src/main/java/com/streamplayer/PlayerActivity.java b/app/src/main/java/com/streamplayer/PlayerActivity.java deleted file mode 100644 index dc60596..0000000 --- a/app/src/main/java/com/streamplayer/PlayerActivity.java +++ /dev/null @@ -1,279 +0,0 @@ -package com.streamplayer; - -import android.content.Intent; -import android.os.Bundle; -import android.os.StrictMode; -import android.view.View; -import android.view.WindowManager; -import android.widget.Button; -import android.widget.ProgressBar; -import android.widget.TextView; - -import androidx.appcompat.app.AppCompatActivity; - -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.MediaItem; -import com.google.android.exoplayer2.PlaybackException; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.DefaultRenderersFactory; -import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.ui.PlayerView; -import com.google.android.exoplayer2.util.Util; - -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.dnsoverhttps.DnsOverHttps; - -public class PlayerActivity extends AppCompatActivity { - - public static final String EXTRA_CHANNEL_NAME = "extra_channel_name"; - public static final String EXTRA_CHANNEL_URL = "extra_channel_url"; - - private PlayerView playerView; - private ProgressBar loadingIndicator; - private TextView errorMessage; - private TextView channelLabel; - private Button closeButton; - private View playerToolbar; - - private ExoPlayer player; - private String channelName; - private String channelUrl; - private boolean overlayVisible = true; - private OkHttpClient okHttpClient; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - StrictMode.setThreadPolicy( - new StrictMode.ThreadPolicy.Builder().permitAll().build() - ); - - setContentView(R.layout.activity_player); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - - Intent intent = getIntent(); - if (intent == null) { - finish(); - return; - } - - channelName = intent.getStringExtra(EXTRA_CHANNEL_NAME); - channelUrl = intent.getStringExtra(EXTRA_CHANNEL_URL); - - if (channelName == null || channelUrl == null) { - finish(); - return; - } - - initViews(); - channelLabel.setText(channelName); - - DNSSetter.configureDNSToGoogle(this); - loadChannel(); - } - - private void initViews() { - playerView = findViewById(R.id.player_view); - loadingIndicator = findViewById(R.id.loading_indicator); - errorMessage = findViewById(R.id.error_message); - channelLabel = findViewById(R.id.player_channel_label); - closeButton = findViewById(R.id.close_button); - playerToolbar = findViewById(R.id.player_toolbar); - - closeButton.setOnClickListener(v -> finish()); - playerView.setOnClickListener(v -> toggleOverlay()); - } - - private void loadChannel() { - showLoading(true); - new Thread(() -> { - try { - String resolvedUrl = StreamUrlResolver.resolve(channelUrl); - runOnUiThread(() -> startPlayback(resolvedUrl)); - } catch (Exception e) { - runOnUiThread(() -> showError("Error al obtener stream: " + e.getMessage())); - } - }).start(); - } - - private void startPlayback(String streamUrl) { - try { - releasePlayer(); - DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this) - .setEnableDecoderFallback(true) - .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON); - player = new ExoPlayer.Builder(this, renderersFactory) - .setSeekForwardIncrementMs(10_000) - .setSeekBackIncrementMs(10_000) - .build(); - playerView.setPlayer(player); - - player.addListener(new Player.Listener() { - @Override - public void onPlaybackStateChanged(int playbackState) { - if (playbackState == Player.STATE_READY) { - showLoading(false); - } else if (playbackState == Player.STATE_BUFFERING) { - showLoading(true); - } - } - - @Override - public void onPlayerError(PlaybackException error) { - String detail = error.getCause() != null ? - error.getCause().getMessage() : ""; - showError("Error al reproducir: " + error.getMessage() + " " + detail); - } - }); - - MediaItem mediaItem = MediaItem.fromUri(streamUrl); - player.setMediaSource(buildMediaSource(mediaItem)); - player.prepare(); - player.setPlayWhenReady(true); - setOverlayVisible(false); - - } catch (Exception e) { - showError("Error al inicializar reproductor: " + e.getMessage()); - } - } - - private void showLoading(boolean show) { - loadingIndicator.setVisibility(show ? View.VISIBLE : View.GONE); - errorMessage.setVisibility(View.GONE); - playerView.setVisibility(show ? View.GONE : View.VISIBLE); - if (show) { - setOverlayVisible(true); - } - } - - private void showError(String message) { - loadingIndicator.setVisibility(View.GONE); - playerView.setVisibility(View.GONE); - errorMessage.setVisibility(View.VISIBLE); - errorMessage.setText(message); - setOverlayVisible(true); - } - - private void releasePlayer() { - if (player != null) { - player.release(); - player = null; - } - } - - private MediaSource buildMediaSource(MediaItem mediaItem) { - Map headers = new HashMap<>(); - headers.put("Referer", channelUrl); - headers.put("Origin", "https://streamtpmedia.com"); - headers.put("Accept", "*/*"); - headers.put("Connection", "keep-alive"); - - String userAgent = Util.getUserAgent(this, "StreamPlayer"); - - OkHttpDataSource.Factory factory = new OkHttpDataSource.Factory(provideOkHttpClient()) - .setUserAgent(userAgent) - .setDefaultRequestProperties(headers); - return new HlsMediaSource.Factory(factory).createMediaSource(mediaItem); - } - - private OkHttpClient provideOkHttpClient() { - if (okHttpClient != null) { - return okHttpClient; - } - - try { - OkHttpClient bootstrap = new OkHttpClient.Builder() - .connectTimeout(15, TimeUnit.SECONDS) - .readTimeout(15, TimeUnit.SECONDS) - .retryOnConnectionFailure(true) - .build(); - - DnsOverHttps dohDns = new DnsOverHttps.Builder() - .client(bootstrap) - .url(HttpUrl.get("https://dns.adguard-dns.com/dns-query")) - .bootstrapDnsHosts( - InetAddress.getByName("94.140.14.14"), - InetAddress.getByName("94.140.15.15")) - .build(); - - okHttpClient = bootstrap.newBuilder() - .dns(dohDns) - .build(); - } catch (UnknownHostException e) { - okHttpClient = new OkHttpClient.Builder() - .connectTimeout(15, TimeUnit.SECONDS) - .readTimeout(15, TimeUnit.SECONDS) - .retryOnConnectionFailure(true) - .build(); - } - - return okHttpClient; - } - - @Override - protected void onStart() { - super.onStart(); - if (player != null) { - playerView.onResume(); - } else if (channelUrl != null) { - loadChannel(); - } - } - - @Override - protected void onResume() { - super.onResume(); - if (player != null) { - playerView.onResume(); - } - } - - @Override - protected void onPause() { - super.onPause(); - if (player != null) { - playerView.onPause(); - } - } - - @Override - protected void onStop() { - super.onStop(); - releasePlayer(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - releasePlayer(); - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - - private void toggleOverlay() { - setOverlayVisible(!overlayVisible); - } - - private void setOverlayVisible(boolean visible) { - overlayVisible = visible; - playerToolbar.setVisibility(visible ? View.VISIBLE : View.GONE); - } - - @Override - public void onBackPressed() { - if (!overlayVisible) { - setOverlayVisible(true); - } else { - super.onBackPressed(); - } - } -} diff --git a/app/src/main/java/com/streamplayer/SectionAdapter.java b/app/src/main/java/com/streamplayer/SectionAdapter.java deleted file mode 100644 index 3832c28..0000000 --- a/app/src/main/java/com/streamplayer/SectionAdapter.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.streamplayer; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import java.util.List; - -public class SectionAdapter extends RecyclerView.Adapter { - - public interface OnSectionSelectedListener { - void onSectionSelected(int position); - } - - private final List sections; - private final OnSectionSelectedListener listener; - private int selectedIndex = 0; - - public SectionAdapter(List sections, OnSectionSelectedListener listener) { - this.sections = sections; - this.listener = listener; - } - - @NonNull - @Override - public SectionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_section, parent, false); - return new SectionViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull SectionViewHolder holder, int position) { - holder.title.setText(sections.get(position)); - holder.itemView.setSelected(position == selectedIndex); - holder.itemView.setOnClickListener(v -> notifySelection(holder)); - holder.itemView.setOnFocusChangeListener((v, hasFocus) -> { - if (hasFocus) { - notifySelection(holder); - } - }); - } - - @Override - public int getItemCount() { - return sections.size(); - } - - public void setSelectedIndex(int index) { - if (index < 0 || index >= sections.size()) { - return; - } - if (selectedIndex == index) { - return; - } - int previous = selectedIndex; - selectedIndex = index; - notifyItemChanged(previous); - notifyItemChanged(selectedIndex); - } - - public int getSelectedIndex() { - return selectedIndex; - } - - private void notifySelection(SectionViewHolder holder) { - int position = holder.getBindingAdapterPosition(); - if (position == RecyclerView.NO_POSITION) { - return; - } - if (listener != null) { - listener.onSectionSelected(position); - } - } - - static class SectionViewHolder extends RecyclerView.ViewHolder { - final TextView title; - - SectionViewHolder(@NonNull View itemView) { - super(itemView); - title = itemView.findViewById(R.id.section_title); - } - } -} diff --git a/app/src/main/java/com/streamplayer/StreamChannel.java b/app/src/main/java/com/streamplayer/StreamChannel.java deleted file mode 100644 index beb6025..0000000 --- a/app/src/main/java/com/streamplayer/StreamChannel.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.streamplayer; - -public class StreamChannel { - private final String name; - private final String pageUrl; - - public StreamChannel(String name, String pageUrl) { - this.name = name; - this.pageUrl = pageUrl; - } - - public String getName() { - return name; - } - - public String getPageUrl() { - return pageUrl; - } -} diff --git a/app/src/main/java/com/streamplayer/StreamUrlResolver.java b/app/src/main/java/com/streamplayer/StreamUrlResolver.java deleted file mode 100644 index 203f839..0000000 --- a/app/src/main/java/com/streamplayer/StreamUrlResolver.java +++ /dev/null @@ -1,132 +0,0 @@ -package com.streamplayer; - -import android.util.Base64; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Resuelve la URL real del stream analizando el JavaScript ofuscado de streamtpmedia. - */ -public final class StreamUrlResolver { - - private static final Pattern ARRAY_NAME_PATTERN = - Pattern.compile("var\\s+playbackURL\\s*=\\s*\"\"\\s*,\\s*([A-Za-z0-9]+)\\s*=\\s*\\[\\]"); - private static final Pattern ENTRY_PATTERN = Pattern.compile("\\[(\\d+),\"([A-Za-z0-9+/=]+)\"\\]"); - private static final Pattern KEY_FUNCTIONS_PATTERN = Pattern.compile("var\\s+k=(\\w+)\\(\\)\\+(\\w+)\\(\\);"); - private static final String FUNCTION_TEMPLATE = "function\\s+%s\\(\\)\\s*\\{\\s*return\\s+(\\d+);\\s*\\}"; - private static final String USER_AGENT = "Mozilla/5.0 (Linux; Android 13) ExoPlayerResolver/1.0"; - - private StreamUrlResolver() { - } - - public static String resolve(String pageUrl) throws IOException { - String html = downloadPage(pageUrl); - long keyOffset = extractKeyOffset(html); - List entries = extractEntries(html); - if (entries.isEmpty()) { - throw new IOException("No se pudieron obtener los fragmentos del stream"); - } - - StringBuilder builder = new StringBuilder(); - for (Entry entry : entries) { - String decoded = new String(Base64.decode(entry.encoded, Base64.DEFAULT), StandardCharsets.UTF_8); - String numeric = decoded.replaceAll("\\D+", ""); - if (numeric.isEmpty()) { - continue; - } - long value = Long.parseLong(numeric) - keyOffset; - builder.append((char) value); - } - String url = builder.toString(); - if (url.isEmpty()) { - throw new IOException("No se pudo reconstruir la URL del stream"); - } - return url; - } - - private static String downloadPage(String pageUrl) throws IOException { - HttpURLConnection connection = (HttpURLConnection) new URL(pageUrl).openConnection(); - connection.setConnectTimeout(15000); - connection.setReadTimeout(15000); - connection.setRequestProperty("User-Agent", USER_AGENT); - connection.setRequestProperty("Accept", "text/html,application/xhtml+xml"); - connection.connect(); - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) { - StringBuilder builder = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - builder.append(line); - } - return builder.toString(); - } finally { - connection.disconnect(); - } - } - - private static long extractKeyOffset(String html) throws IOException { - Matcher matcher = KEY_FUNCTIONS_PATTERN.matcher(html); - if (!matcher.find()) { - throw new IOException("No se encontró la clave del stream"); - } - String first = matcher.group(1); - String second = matcher.group(2); - long firstVal = extractReturnValue(html, first); - long secondVal = extractReturnValue(html, second); - return firstVal + secondVal; - } - - private static long extractReturnValue(String html, String functionName) throws IOException { - Pattern functionPattern = Pattern.compile( - String.format(FUNCTION_TEMPLATE, Pattern.quote(functionName))); - Matcher matcher = functionPattern.matcher(html); - if (!matcher.find()) { - throw new IOException("No se encontró el valor de la función " + functionName); - } - return Long.parseLong(matcher.group(1)); - } - - private static List extractEntries(String html) throws IOException { - Matcher arrayNameMatcher = ARRAY_NAME_PATTERN.matcher(html); - if (!arrayNameMatcher.find()) { - throw new IOException("No se detectó la variable del arreglo de fragmentos"); - } - String arrayName = arrayNameMatcher.group(1); - Pattern arrayPattern = Pattern.compile(Pattern.quote(arrayName) + "=\\[(.*?)\\];", Pattern.DOTALL); - Matcher matcher = arrayPattern.matcher(html); - if (!matcher.find()) { - throw new IOException("No se encontró el arreglo de fragmentos"); - } - String rawEntries = matcher.group(1); - Matcher entryMatcher = ENTRY_PATTERN.matcher(rawEntries); - List entries = new ArrayList<>(); - while (entryMatcher.find()) { - int index = Integer.parseInt(entryMatcher.group(1)); - String encoded = entryMatcher.group(2); - entries.add(new Entry(index, encoded)); - } - Collections.sort(entries, Comparator.comparingInt(e -> e.index)); - return entries; - } - - private static final class Entry { - final int index; - final String encoded; - - Entry(int index, String encoded) { - this.index = index; - this.encoded = encoded; - } - } -} diff --git a/app/src/main/java/com/streamplayer/UpdateManager.java b/app/src/main/java/com/streamplayer/UpdateManager.java deleted file mode 100644 index 71043e6..0000000 --- a/app/src/main/java/com/streamplayer/UpdateManager.java +++ /dev/null @@ -1,518 +0,0 @@ -package com.streamplayer; - -import android.app.Activity; -import android.app.DownloadManager; -import android.content.ActivityNotFoundException; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.database.Cursor; -import android.net.Uri; -import android.os.Build; -import android.os.Environment; -import android.os.Handler; -import android.os.Looper; -import android.provider.Settings; -import android.text.TextUtils; -import android.text.format.Formatter; -import android.util.Log; -import android.widget.Toast; - -import androidx.core.content.FileProvider; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.File; -import java.io.IOException; -import java.lang.ref.WeakReference; -import java.util.Locale; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; - -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; - -/** - * Encapsula toda la lógica para consultar releases de Gitea, descargar el APK y lanzarlo. - */ -public class UpdateManager { - - private static final String TAG = "UpdateManager"; - private static final String LATEST_RELEASE_URL = - "https://gitea.cbcren.online/api/v1/repos/renato97/app/releases/latest"; - - private final Context appContext; - private final Handler mainHandler; - private final ExecutorService networkExecutor; - private final OkHttpClient httpClient; - - private WeakReference activityRef; - private DownloadReceiver downloadReceiver; - private long currentDownloadId = -1L; - private File downloadingFile; - private File pendingInstallFile; - private UpdateInfo cachedUpdate; - - public UpdateManager(Context context) { - this.appContext = context.getApplicationContext(); - this.mainHandler = new Handler(Looper.getMainLooper()); - this.networkExecutor = Executors.newSingleThreadExecutor(); - this.httpClient = new OkHttpClient.Builder() - .connectTimeout(15, TimeUnit.SECONDS) - .readTimeout(20, TimeUnit.SECONDS) - .callTimeout(25, TimeUnit.SECONDS) - .build(); - } - - public void checkForUpdates(UpdateCallback callback) { - networkExecutor.execute(() -> { - try { - Request request = new Request.Builder() - .url(LATEST_RELEASE_URL) - .get() - .build(); - try (Response response = httpClient.newCall(request).execute()) { - if (response.body() == null) { - postError(callback, appContext.getString(R.string.update_error_empty_response)); - return; - } - if (!response.isSuccessful()) { - postError(callback, appContext.getString(R.string.update_error_http, - response.code())); - return; - } - String body = response.body().string(); - UpdateInfo info = parseRelease(body); - cachedUpdate = info; - if (info != null && info.isUpdateAvailable(BuildConfig.VERSION_CODE)) { - postAvailable(callback, info); - } else { - postUpToDate(callback); - } - } - } catch (IOException | JSONException e) { - Log.w(TAG, "Error checking updates", e); - postError(callback, e.getMessage()); - } - }); - } - - public UpdateInfo getCachedUpdate() { - return cachedUpdate; - } - - public void downloadUpdate(Activity activity, UpdateInfo info) { - if (info == null || TextUtils.isEmpty(info.downloadUrl)) { - showToast(appContext.getString(R.string.update_error_missing_url)); - return; - } - DownloadManager downloadManager = - (DownloadManager) appContext.getSystemService(Context.DOWNLOAD_SERVICE); - if (downloadManager == null) { - showToast(appContext.getString(R.string.update_error_download_manager)); - return; - } - File targetDir = appContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS); - if (targetDir == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - targetDir = appContext.getExternalFilesDir(null); - } - if (targetDir == null) { - showToast(appContext.getString(R.string.update_error_storage)); - return; - } - if (!targetDir.exists() && !targetDir.mkdirs()) { - showToast(appContext.getString(R.string.update_error_storage)); - return; - } - String fileName = info.getResolvedFileName(); - File apkFile = new File(targetDir, fileName); - if (apkFile.exists() && !apkFile.delete()) { - showToast(appContext.getString(R.string.update_error_storage)); - return; - } - - Uri destination = Uri.fromFile(apkFile); - DownloadManager.Request request = new DownloadManager.Request(Uri.parse(info.downloadUrl)); - request.setTitle(appContext.getString(R.string.update_notification_title, info.versionName)); - request.setDescription(appContext.getString(R.string.update_notification_description)); - request.setAllowedOverMetered(true); - request.setAllowedOverRoaming(false); - request.setNotificationVisibility( - DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); - request.setDestinationUri(destination); - - try { - currentDownloadId = downloadManager.enqueue(request); - } catch (IllegalArgumentException e) { - showToast(appContext.getString(R.string.update_error_download, e.getMessage())); - return; - } - downloadingFile = apkFile; - activityRef = new WeakReference<>(activity); - registerDownloadReceiver(); - showToast(appContext.getString(R.string.update_download_started)); - } - - public void resumePendingInstall(Activity activity) { - if (pendingInstallFile == null || !pendingInstallFile.exists()) { - return; - } - installDownloadedApk(activity, pendingInstallFile); - } - - public void release() { - unregisterDownloadReceiver(); - networkExecutor.shutdownNow(); - activityRef = null; - } - - private UpdateInfo parseRelease(String responseBody) throws JSONException, IOException { - JSONObject releaseJson = new JSONObject(responseBody); - String tagName = releaseJson.optString("tag_name", ""); - String versionName = deriveVersionName(tagName, releaseJson.optString("name")); - int versionCode = parseVersionCode(versionName); - String releaseNotes = releaseJson.optString("body", ""); - String releasePageUrl = releaseJson.optString("html_url", ""); - JSONArray assets = releaseJson.optJSONArray("assets"); - JSONObject apkAsset = findApkAsset(assets); - String downloadUrl = apkAsset != null ? apkAsset.optString("browser_download_url", "") : ""; - String downloadFileName = apkAsset != null ? apkAsset.optString("name", "") : ""; - long sizeBytes = apkAsset != null ? apkAsset.optLong("size", 0L) : 0L; - int downloadCount = apkAsset != null ? apkAsset.optInt("download_count", 0) : 0; - - int minSupported = 0; - boolean forceUpdate = false; - JSONObject manifestJson = fetchManifest(assets); - if (manifestJson != null) { - versionCode = manifestJson.optInt("versionCode", versionCode); - versionName = manifestJson.optString("versionName", versionName); - minSupported = manifestJson.optInt("minSupportedVersionCode", 0); - forceUpdate = manifestJson.optBoolean("forceUpdate", false); - String manifestUrl = manifestJson.optString("downloadUrl", null); - if (!TextUtils.isEmpty(manifestUrl)) { - downloadUrl = manifestUrl; - } - if (manifestJson.has("fileName")) { - downloadFileName = manifestJson.optString("fileName", downloadFileName); - } - if (manifestJson.has("sizeBytes")) { - sizeBytes = manifestJson.optLong("sizeBytes", sizeBytes); - } - if (manifestJson.has("notes") && TextUtils.isEmpty(releaseNotes)) { - releaseNotes = manifestJson.optString("notes", releaseNotes); - } - } - - if (TextUtils.isEmpty(downloadUrl)) { - return null; - } - - return new UpdateInfo(versionCode, versionName, releaseNotes, downloadUrl, - downloadFileName, sizeBytes, downloadCount, releasePageUrl, - minSupported, forceUpdate); - } - - private JSONObject fetchManifest(JSONArray assets) throws IOException, JSONException { - if (assets == null) { - return null; - } - for (int i = 0; i < assets.length(); i++) { - JSONObject asset = assets.optJSONObject(i); - if (asset == null) { - continue; - } - String name = asset.optString("name", "").toLowerCase(Locale.US); - if (TextUtils.isEmpty(name) || !name.endsWith(".json")) { - continue; - } - if (!(name.contains("update") || name.contains("manifest"))) { - continue; - } - String url = asset.optString("browser_download_url", ""); - if (TextUtils.isEmpty(url)) { - continue; - } - Request request = new Request.Builder().url(url).get().build(); - try (Response response = httpClient.newCall(request).execute()) { - if (!response.isSuccessful() || response.body() == null) { - continue; - } - String json = response.body().string(); - if (!TextUtils.isEmpty(json)) { - return new JSONObject(json); - } - } - } - return null; - } - - private JSONObject findApkAsset(JSONArray assets) { - if (assets == null) { - return null; - } - JSONObject fallback = null; - for (int i = 0; i < assets.length(); i++) { - JSONObject asset = assets.optJSONObject(i); - if (asset == null) { - continue; - } - if (fallback == null) { - fallback = asset; - } - String name = asset.optString("name", "").toLowerCase(Locale.US); - if (name.endsWith(".apk")) { - return asset; - } - } - return fallback; - } - - private String deriveVersionName(String tagName, String fallback) { - String base = !TextUtils.isEmpty(tagName) ? tagName : fallback; - if (TextUtils.isEmpty(base)) { - return ""; - } - return base.replaceFirst("^[Vv]", "").trim(); - } - - private int parseVersionCode(String versionName) { - if (TextUtils.isEmpty(versionName)) { - return -1; - } - String normalized = versionName.replaceAll("[^0-9\\.]", ""); - if (TextUtils.isEmpty(normalized)) { - return -1; - } - String[] parts = normalized.split("\\."); - int major = parsePart(parts, 0); - int minor = parsePart(parts, 1); - int patch = parsePart(parts, 2); - return major * 10000 + minor * 100 + patch; - } - - private int parsePart(String[] parts, int index) { - if (parts.length <= index) { - return 0; - } - try { - return Integer.parseInt(parts[index]); - } catch (NumberFormatException e) { - return 0; - } - } - - private void installDownloadedApk(Activity activity, File apkFile) { - if (activity == null || apkFile == null || !apkFile.exists()) { - return; - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - boolean canInstall = appContext.getPackageManager().canRequestPackageInstalls(); - if (!canInstall) { - pendingInstallFile = apkFile; - Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, - Uri.parse("package:" + appContext.getPackageName())); - try { - activity.startActivity(intent); - } catch (ActivityNotFoundException ignored) { - showToast(appContext.getString(R.string.update_error_install_permissions)); - } - showToast(appContext.getString(R.string.update_permission_request)); - return; - } - } - Uri uri = FileProvider.getUriForFile(appContext, - appContext.getPackageName() + ".fileprovider", apkFile); - Intent installIntent = new Intent(Intent.ACTION_VIEW); - installIntent.setDataAndType(uri, "application/vnd.android.package-archive"); - installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - try { - activity.startActivity(installIntent); - pendingInstallFile = null; - } catch (ActivityNotFoundException e) { - showToast(appContext.getString(R.string.update_error_install_intent)); - } - } - - private void registerDownloadReceiver() { - if (downloadReceiver != null) { - return; - } - downloadReceiver = new DownloadReceiver(); - IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE); - appContext.registerReceiver(downloadReceiver, filter); - } - - private void unregisterDownloadReceiver() { - if (downloadReceiver != null) { - try { - appContext.unregisterReceiver(downloadReceiver); - } catch (IllegalArgumentException ignored) { - } - downloadReceiver = null; - } - } - - private void handleDownloadComplete(long downloadId) { - if (downloadId != currentDownloadId) { - return; - } - DownloadManager downloadManager = - (DownloadManager) appContext.getSystemService(Context.DOWNLOAD_SERVICE); - if (downloadManager == null) { - showToast(appContext.getString(R.string.update_error_download_manager)); - cleanupDownloadState(); - return; - } - DownloadManager.Query query = new DownloadManager.Query(); - query.setFilterById(downloadId); - Cursor cursor = downloadManager.query(query); - if (cursor != null) { - try { - if (cursor.moveToFirst()) { - int status = cursor.getInt(cursor.getColumnIndexOrThrow( - DownloadManager.COLUMN_STATUS)); - if (status == DownloadManager.STATUS_SUCCESSFUL && downloadingFile != null) { - pendingInstallFile = downloadingFile; - Activity activity = activityRef != null ? activityRef.get() : null; - mainHandler.post(() -> { - showToast(appContext.getString(R.string.update_download_complete)); - installDownloadedApk(activity, pendingInstallFile); - }); - } else { - int reason = cursor.getInt(cursor.getColumnIndexOrThrow( - DownloadManager.COLUMN_REASON)); - mainHandler.post(() -> showToast(appContext.getString( - R.string.update_error_download_failed, reason))); - } - } - } finally { - cursor.close(); - } - } - cleanupDownloadState(); - } - - private void cleanupDownloadState() { - unregisterDownloadReceiver(); - currentDownloadId = -1L; - downloadingFile = null; - } - - private void postAvailable(UpdateCallback callback, UpdateInfo info) { - if (callback == null) { - return; - } - mainHandler.post(() -> callback.onUpdateAvailable(info)); - } - - private void postUpToDate(UpdateCallback callback) { - if (callback == null) { - return; - } - mainHandler.post(callback::onUpToDate); - } - - private void postError(UpdateCallback callback, String message) { - if (callback == null) { - return; - } - mainHandler.post(() -> callback.onError(message)); - } - - private void showToast(String message) { - mainHandler.post(() -> Toast.makeText(appContext, message, Toast.LENGTH_LONG).show()); - } - - private class DownloadReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1L); - handleDownloadComplete(id); - } - } - - public interface UpdateCallback { - void onUpdateAvailable(UpdateInfo info); - - void onUpToDate(); - - void onError(String message); - } - - public static class UpdateInfo { - public final int versionCode; - public final String versionName; - public final String releaseNotes; - public final String downloadUrl; - public final String downloadFileName; - public final long downloadSizeBytes; - public final int downloadCount; - public final String releasePageUrl; - public final int minSupportedVersionCode; - public final boolean forceUpdate; - - UpdateInfo(int versionCode, - String versionName, - String releaseNotes, - String downloadUrl, - String downloadFileName, - long downloadSizeBytes, - int downloadCount, - String releasePageUrl, - int minSupportedVersionCode, - boolean forceUpdate) { - this.versionCode = versionCode; - this.versionName = versionName; - this.releaseNotes = releaseNotes == null ? "" : releaseNotes.trim(); - this.downloadUrl = downloadUrl; - this.downloadFileName = downloadFileName; - this.downloadSizeBytes = downloadSizeBytes; - this.downloadCount = downloadCount; - this.releasePageUrl = releasePageUrl; - this.minSupportedVersionCode = minSupportedVersionCode; - this.forceUpdate = forceUpdate; - } - - public boolean isUpdateAvailable(int currentVersionCode) { - return versionCode > currentVersionCode; - } - - public boolean isMandatory(int currentVersionCode) { - return forceUpdate || currentVersionCode < minSupportedVersionCode; - } - - public String getReleaseNotesPreview() { - if (TextUtils.isEmpty(releaseNotes)) { - return ""; - } - final int limit = 900; - if (releaseNotes.length() <= limit) { - return releaseNotes; - } - return releaseNotes.substring(0, limit) + "\n…"; - } - - public String getResolvedFileName() { - if (!TextUtils.isEmpty(downloadFileName)) { - return downloadFileName; - } - String safeVersion = TextUtils.isEmpty(versionName) ? String.valueOf(versionCode) - : versionName.replaceAll("[^0-9a-zA-Z._-]", ""); - return "StreamPlayer-" + safeVersion + ".apk"; - } - - public String formatSize(Context context) { - if (downloadSizeBytes <= 0) { - return ""; - } - return Formatter.formatShortFileSize(context, downloadSizeBytes); - } - } -} - diff --git a/app/src/main/res/color/section_text_selector.xml b/app/src/main/res/color/section_text_selector.xml deleted file mode 100644 index c20609b..0000000 --- a/app/src/main/res/color/section_text_selector.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/banner_streamplayer.xml b/app/src/main/res/drawable/banner_streamplayer.xml deleted file mode 100644 index ae786fd..0000000 --- a/app/src/main/res/drawable/banner_streamplayer.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/bg_channel_item_selector.xml b/app/src/main/res/drawable/bg_channel_item_selector.xml deleted file mode 100644 index a31cbae..0000000 --- a/app/src/main/res/drawable/bg_channel_item_selector.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/bg_dialog_button_primary.xml b/app/src/main/res/drawable/bg_dialog_button_primary.xml deleted file mode 100644 index 9f798b1..0000000 --- a/app/src/main/res/drawable/bg_dialog_button_primary.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/bg_dialog_button_secondary.xml b/app/src/main/res/drawable/bg_dialog_button_secondary.xml deleted file mode 100644 index c7caaf0..0000000 --- a/app/src/main/res/drawable/bg_dialog_button_secondary.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/bg_dialog_dark.xml b/app/src/main/res/drawable/bg_dialog_dark.xml deleted file mode 100644 index 7278096..0000000 --- a/app/src/main/res/drawable/bg_dialog_dark.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/bg_events_refresh_button.xml b/app/src/main/res/drawable/bg_events_refresh_button.xml deleted file mode 100644 index c30d557..0000000 --- a/app/src/main/res/drawable/bg_events_refresh_button.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/bg_section_indicator.xml b/app/src/main/res/drawable/bg_section_indicator.xml deleted file mode 100644 index fdbfdeb..0000000 --- a/app/src/main/res/drawable/bg_section_indicator.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/bg_tab_selector.xml b/app/src/main/res/drawable/bg_tab_selector.xml deleted file mode 100644 index f5d4ae1..0000000 --- a/app/src/main/res/drawable/bg_tab_selector.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_channel_default.xml b/app/src/main/res/drawable/ic_channel_default.xml deleted file mode 100644 index 4d3663b..0000000 --- a/app/src/main/res/drawable/ic_channel_default.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_launcher_foreground.png b/app/src/main/res/drawable/ic_launcher_foreground.png deleted file mode 100644 index 5a8357d..0000000 Binary files a/app/src/main/res/drawable/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index fedbbfd..0000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - - - - - - - - - - - - - - -