diff --git a/app/build.gradle b/app/build.gradle index ef683e8..f76a664 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "com.streamplayer" minSdk 21 targetSdk 35 - versionCode 100200 - versionName "11.0.0" + versionCode 100201 + versionName "11.0.1" buildConfigField "String", "DEVICE_REGISTRY_URL", '"http://194.163.191.200:4000"' } @@ -51,9 +51,6 @@ dependencies { implementation 'androidx.media3:media3-exoplayer-hls:1.4.1' implementation 'androidx.media3:media3-ui:1.4.1' - // VLC Player para reproduccion de video (soporte DRM mejorado) - implementation 'org.videolan.android:libvlc-all:3.4.4' - // OkHttp con DNS over HTTPS (para StreamUrlResolver) implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.12.0' diff --git a/app/src/main/java/com/streamplayer/ChannelAdapter.java b/app/src/main/java/com/streamplayer/ChannelAdapter.java index cc76548..740f1ff 100644 --- a/app/src/main/java/com/streamplayer/ChannelAdapter.java +++ b/app/src/main/java/com/streamplayer/ChannelAdapter.java @@ -7,21 +7,37 @@ import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListAdapter; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; -public class ChannelAdapter extends RecyclerView.Adapter { +public class ChannelAdapter extends ListAdapter { public interface OnChannelClickListener { void onChannelClick(StreamChannel channel); } - private final List channels = new ArrayList<>(); + private static final DiffUtil.ItemCallback DIFF_CALLBACK = + new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame(@NonNull StreamChannel oldItem, @NonNull StreamChannel newItem) { + return oldItem.getPageUrl().equals(newItem.getPageUrl()); + } + + @Override + public boolean areContentsTheSame(@NonNull StreamChannel oldItem, @NonNull StreamChannel newItem) { + return oldItem.getName().equals(newItem.getName()) + && oldItem.getPageUrl().equals(newItem.getPageUrl()); + } + }; + private final OnChannelClickListener listener; public ChannelAdapter(OnChannelClickListener listener) { + super(DIFF_CALLBACK); this.listener = listener; } @@ -35,7 +51,7 @@ public class ChannelAdapter extends RecyclerView.Adapter { @@ -52,7 +68,7 @@ public class ChannelAdapter extends RecyclerView.Adapter newChannels) { - channels.clear(); - if (newChannels != null) { - channels.addAll(newChannels); + if (newChannels == null) { + super.submitList(null); + return; } - notifyDataSetChanged(); + super.submitList(new ArrayList<>(newChannels)); } } diff --git a/app/src/main/java/com/streamplayer/ChannelRepository.java b/app/src/main/java/com/streamplayer/ChannelRepository.java index 6e20352..442df33 100644 --- a/app/src/main/java/com/streamplayer/ChannelRepository.java +++ b/app/src/main/java/com/streamplayer/ChannelRepository.java @@ -9,6 +9,13 @@ import java.util.List; public final class ChannelRepository { private static final List CHANNELS = createChannels(); + private static final Comparator CHANNEL_NAME_COMPARATOR = + new Comparator() { + @Override + public int compare(StreamChannel left, StreamChannel right) { + return String.CASE_INSENSITIVE_ORDER.compare(left.getName(), right.getName()); + } + }; private static List createChannels() { List channels = new ArrayList<>(Arrays.asList( @@ -79,7 +86,7 @@ public final class ChannelRepository { new StreamChannel("FUTV", "http://streamtp10.com/global2.php?stream=futv"), new StreamChannel("LaLiga Hypermotion", "http://streamtp10.com/global2.php?stream=laligahypermotion") )); - channels.sort(Comparator.comparing(StreamChannel::getName, String.CASE_INSENSITIVE_ORDER)); + Collections.sort(channels, CHANNEL_NAME_COMPARATOR); return Collections.unmodifiableList(channels); } diff --git a/app/src/main/java/com/streamplayer/DeviceRegistry.java b/app/src/main/java/com/streamplayer/DeviceRegistry.java index d9de21e..b3af4bc 100644 --- a/app/src/main/java/com/streamplayer/DeviceRegistry.java +++ b/app/src/main/java/com/streamplayer/DeviceRegistry.java @@ -136,7 +136,7 @@ public class DeviceRegistry { if (TextUtils.isEmpty(value)) { return ""; } - return value.substring(0, 1).toUpperCase(Locale.getDefault()) + return value.substring(0, 1).toUpperCase(Locale.ROOT) + value.substring(1); } diff --git a/app/src/main/java/com/streamplayer/EventAdapter.java b/app/src/main/java/com/streamplayer/EventAdapter.java index 845351d..d0085d8 100644 --- a/app/src/main/java/com/streamplayer/EventAdapter.java +++ b/app/src/main/java/com/streamplayer/EventAdapter.java @@ -6,29 +6,53 @@ import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListAdapter; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; import java.util.Locale; -public class EventAdapter extends RecyclerView.Adapter { +public class EventAdapter extends ListAdapter { public interface OnEventClickListener { void onEventClick(EventItem event); } - private final List events = new ArrayList<>(); + private static final DiffUtil.ItemCallback DIFF_CALLBACK = + new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame(@NonNull EventItem oldItem, @NonNull EventItem newItem) { + return oldItem.getPageUrl().equals(newItem.getPageUrl()) + && oldItem.getStartMillis() == newItem.getStartMillis(); + } + + @Override + public boolean areContentsTheSame(@NonNull EventItem oldItem, @NonNull EventItem newItem) { + return oldItem.getTitle().equals(newItem.getTitle()) + && oldItem.getTime().equals(newItem.getTime()) + && oldItem.getCategory().equals(newItem.getCategory()) + && oldItem.getStatus().equals(newItem.getStatus()) + && oldItem.getPageUrl().equals(newItem.getPageUrl()) + && oldItem.getChannelName().equals(newItem.getChannelName()) + && oldItem.getStartMillis() == newItem.getStartMillis(); + } + }; + private final OnEventClickListener listener; public EventAdapter(OnEventClickListener listener) { + super(DIFF_CALLBACK); this.listener = listener; } public void submitList(List newEvents) { - events.clear(); - events.addAll(newEvents); - notifyDataSetChanged(); + if (newEvents == null) { + super.submitList(null); + return; + } + super.submitList(new ArrayList<>(newEvents)); } @NonNull @@ -41,7 +65,7 @@ public class EventAdapter extends RecyclerView.Adapter events = new ArrayList<>(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm"); for (int i = 0; i < array.length(); i++) { JSONObject obj = array.getJSONObject(i); @@ -129,19 +124,10 @@ public class EventRepository { String status = obj.optString("status"); String link = obj.optString("link"); String normalized = normalizeLink(link); - - // Ajustar hora: la web muestra hora de España, Argentina es +2 horas - String displayTime = time; - try { - if (time != null && !time.isEmpty()) { - LocalTime localTime = LocalTime.parse(time.trim(), formatter); - LocalTime adjustedTime = localTime.plusHours(2); - displayTime = adjustedTime.format(formatter); - } - } catch (DateTimeParseException ignored) { - } - - long startMillis = parseEventTime(time); + + EventSchedule schedule = computeEventSchedule(time); + String displayTime = schedule.displayTime; + long startMillis = schedule.startMillis; events.add(new EventItem(title, displayTime, category, status, normalized, extractChannelName(link), startMillis)); } return Collections.unmodifiableList(events); @@ -164,28 +150,69 @@ public class EventRepository { if (idx == -1) { return ""; } - return link.substring(idx + 7).replace("_", " ").toUpperCase(); + return link.substring(idx + 7).replace("_", " ").toUpperCase(Locale.ROOT); } - private long parseEventTime(String time) { - if (time == null || time.isEmpty()) { - return -1; + private EventSchedule computeEventSchedule(String time) { + if (time == null || time.trim().isEmpty()) { + return new EventSchedule(time == null ? "" : time, -1L); } + try { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm"); - LocalTime localTime = LocalTime.parse(time.trim(), formatter); - // Ajustar hora: la web muestra hora de España, Argentina es +2 horas - LocalTime adjustedTime = localTime.plusHours(2); - ZoneId zone = ZoneId.of("America/Argentina/Buenos_Aires"); - LocalDate today = LocalDate.now(zone); - ZonedDateTime start = ZonedDateTime.of(LocalDateTime.of(today, adjustedTime), zone); - ZonedDateTime now = ZonedDateTime.now(zone); - if (start.isBefore(now.minusHours(12))) { - start = start.plusDays(1); + Calendar adjustedTime = parseAdjustedTime(time); + String displayTime = formatTime(adjustedTime); + + Calendar now = Calendar.getInstance(ARGENTINA_TIMEZONE, Locale.US); + Calendar start = Calendar.getInstance(ARGENTINA_TIMEZONE, Locale.US); + start.set(Calendar.YEAR, now.get(Calendar.YEAR)); + start.set(Calendar.MONTH, now.get(Calendar.MONTH)); + start.set(Calendar.DAY_OF_MONTH, now.get(Calendar.DAY_OF_MONTH)); + start.set(Calendar.HOUR_OF_DAY, adjustedTime.get(Calendar.HOUR_OF_DAY)); + start.set(Calendar.MINUTE, adjustedTime.get(Calendar.MINUTE)); + start.set(Calendar.SECOND, 0); + start.set(Calendar.MILLISECOND, 0); + + long nowMillis = now.getTimeInMillis(); + long startMillis = start.getTimeInMillis(); + if (startMillis < nowMillis - EVENT_ROLLOVER_WINDOW_MS) { + start.add(Calendar.DAY_OF_MONTH, 1); + startMillis = start.getTimeInMillis(); } - return start.toInstant().toEpochMilli(); - } catch (DateTimeParseException e) { - return -1; + + return new EventSchedule(displayTime, startMillis); + } catch (ParseException ignored) { + return new EventSchedule(time, -1L); + } + } + + private Calendar parseAdjustedTime(String time) throws ParseException { + SimpleDateFormat parser = new SimpleDateFormat("HH:mm", Locale.US); + parser.setLenient(false); + parser.setTimeZone(ARGENTINA_TIMEZONE); + java.util.Date parsedDate = parser.parse(time.trim()); + if (parsedDate == null) { + throw new ParseException("Hora inválida: " + time, 0); + } + + Calendar adjusted = Calendar.getInstance(ARGENTINA_TIMEZONE, Locale.US); + adjusted.setTime(parsedDate); + adjusted.add(Calendar.HOUR_OF_DAY, ARGENTINA_OFFSET_HOURS); + return adjusted; + } + + private String formatTime(Calendar calendar) { + SimpleDateFormat formatter = new SimpleDateFormat("HH:mm", Locale.US); + formatter.setTimeZone(ARGENTINA_TIMEZONE); + return formatter.format(calendar.getTime()); + } + + private static final class EventSchedule { + final String displayTime; + final long startMillis; + + EventSchedule(String displayTime, long startMillis) { + this.displayTime = displayTime; + this.startMillis = startMillis; } } } diff --git a/app/src/main/java/com/streamplayer/MainActivity.java b/app/src/main/java/com/streamplayer/MainActivity.java index 9c82258..8bd7e12 100644 --- a/app/src/main/java/com/streamplayer/MainActivity.java +++ b/app/src/main/java/com/streamplayer/MainActivity.java @@ -1,6 +1,7 @@ package com.streamplayer; import android.app.AlertDialog; +import android.content.ActivityNotFoundException; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; @@ -333,7 +334,7 @@ public class MainActivity extends AppCompatActivity { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); try { startActivity(intent); - } catch (Exception e) { + } catch (ActivityNotFoundException e) { Toast.makeText(this, R.string.update_error_open_release, Toast.LENGTH_SHORT).show(); } } @@ -398,7 +399,12 @@ public class MainActivity extends AppCompatActivity { List allChannels = ChannelRepository.getChannels(); for (StreamChannel channel : allChannels) { String key = deriveGroupName(channel.getName()); - grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(channel); + List group = grouped.get(key); + if (group == null) { + group = new ArrayList<>(); + grouped.put(key, group); + } + group.add(channel); } List espnChannels = grouped.remove("ESPN"); diff --git a/app/src/main/java/com/streamplayer/NetworkUtils.java b/app/src/main/java/com/streamplayer/NetworkUtils.java index 59f154e..a0e9b53 100644 --- a/app/src/main/java/com/streamplayer/NetworkUtils.java +++ b/app/src/main/java/com/streamplayer/NetworkUtils.java @@ -1,7 +1,8 @@ package com.streamplayer; +import android.util.Log; + import java.net.InetAddress; -import java.net.UnknownHostException; import java.security.cert.X509Certificate; import java.util.List; import java.util.concurrent.TimeUnit; @@ -19,8 +20,9 @@ import okhttp3.dnsoverhttps.DnsOverHttps; * Utilidad centralizada para configuración de red. * Fuerza DNS over HTTPS con fallback Google -> Cloudflare -> DNS del sistema. */ -public class NetworkUtils { +public final class NetworkUtils { + private static final String TAG = "NetworkUtils"; private static final OkHttpClient CLIENT; private static final String USER_AGENT = "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36"; private static final String GOOGLE_DOH_URL = "https://dns.google/dns-query"; @@ -51,7 +53,7 @@ public class NetworkUtils { } }; - final SSLContext sslContext = SSLContext.getInstance("SSL"); + final SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); builder.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustAllCerts[0]); @@ -105,12 +107,15 @@ public class NetworkUtils { } catch (Exception e) { builder.dns(Dns.SYSTEM); - System.out.println("Error configurando DNS over HTTPS: " + e.getMessage()); + Log.w(TAG, "Error configurando DNS over HTTPS", e); } CLIENT = builder.build(); } + private NetworkUtils() { + } + public static OkHttpClient getClient() { return CLIENT; } diff --git a/app/src/main/java/com/streamplayer/PlayerActivity.java b/app/src/main/java/com/streamplayer/PlayerActivity.java index 93fff25..5ffb70e 100644 --- a/app/src/main/java/com/streamplayer/PlayerActivity.java +++ b/app/src/main/java/com/streamplayer/PlayerActivity.java @@ -5,7 +5,6 @@ import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.os.StrictMode; import android.util.Log; import android.view.View; import android.view.WindowManager; @@ -13,10 +12,12 @@ import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; +import androidx.annotation.OptIn; import androidx.appcompat.app.AppCompatActivity; import androidx.media3.common.MediaItem; import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; +import androidx.media3.common.util.UnstableApi; import androidx.media3.datasource.DefaultHttpDataSource; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.hls.HlsMediaSource; @@ -24,8 +25,10 @@ import androidx.media3.ui.PlayerView; import java.io.IOException; import java.util.HashMap; +import java.util.Locale; import java.util.Map; +@OptIn(markerClass = UnstableApi.class) public class PlayerActivity extends AppCompatActivity { public static final String EXTRA_CHANNEL_NAME = "extra_channel_name"; @@ -58,10 +61,6 @@ public class PlayerActivity extends AppCompatActivity { 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); @@ -261,7 +260,7 @@ public class PlayerActivity extends AppCompatActivity { return; } - String lower = errorMsg.toLowerCase(); + String lower = errorMsg.toLowerCase(Locale.ROOT); boolean isRetryableError = lower.contains("404") || lower.contains("403") || @@ -275,7 +274,7 @@ public class PlayerActivity extends AppCompatActivity { runOnUiThread(() -> { showLoading(true); errorMessage.setVisibility(View.VISIBLE); - errorMessage.setText("Error de conexión. Reintentando... (" + retryCount + "/" + VlcPlayerConfig.MAX_RETRIES + ")"); + errorMessage.setText(getString(R.string.player_retrying, retryCount, VlcPlayerConfig.MAX_RETRIES)); }); mainHandler.postDelayed(() -> { @@ -406,6 +405,7 @@ public class PlayerActivity extends AppCompatActivity { @Override protected void onDestroy() { + mainHandler.removeCallbacksAndMessages(null); super.onDestroy(); releasePlayer(); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); diff --git a/app/src/main/java/com/streamplayer/StreamUrlResolver.java b/app/src/main/java/com/streamplayer/StreamUrlResolver.java index e349490..35a934a 100644 --- a/app/src/main/java/com/streamplayer/StreamUrlResolver.java +++ b/app/src/main/java/com/streamplayer/StreamUrlResolver.java @@ -3,35 +3,24 @@ package com.streamplayer; import android.util.Base64; import java.io.IOException; -import java.net.InetAddress; -import java.security.cert.X509Certificate; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; -import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; - -import okhttp3.Dns; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; -import okhttp3.dnsoverhttps.DnsOverHttps; /** * Resuelve la URL real del stream extrayendo playbackURL de la página. * Utiliza DNS over HTTPS (Google + Cloudflare) para evitar bloqueos. * Soporta múltiples formatos de páginas y streams directos. - * Soporta JWPlayer con DRM ClearKey. + * Incluye fallback para páginas con JWPlayer y formatos ofuscados. */ public final class StreamUrlResolver { @@ -73,93 +62,6 @@ public final class StreamUrlResolver { Pattern.compile("function\\s+([A-Za-z_$][\\w$]*)\\s*\\(\\)\\s*\\{\\s*return\\s*(\\d+)\\s*;?\\s*\\}", Pattern.DOTALL); - private static final String USER_AGENT = "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36"; - - private static final OkHttpClient CLIENT; - - static { - OkHttpClient client = null; - try { - final TrustManager[] trustAllCerts = new TrustManager[]{ - new X509TrustManager() { - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) {} - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType) {} - @Override - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[]{}; - } - } - }; - - final SSLContext sslContext = SSLContext.getInstance("SSL"); - sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); - - OkHttpClient.Builder builder = new OkHttpClient.Builder() - .connectTimeout(15, TimeUnit.SECONDS) - .readTimeout(15, TimeUnit.SECONDS) - .followRedirects(true) - .sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustAllCerts[0]) - .hostnameVerifier((hostname, session) -> true); - - OkHttpClient bootstrap = new OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(10, TimeUnit.SECONDS) - .sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustAllCerts[0]) - .hostnameVerifier((hostname, session) -> true) - .build(); - - final DnsOverHttps googleDns = new DnsOverHttps.Builder() - .client(bootstrap) - .url(HttpUrl.get("https://dns.google/dns-query")) - .bootstrapDnsHosts( - InetAddress.getByName("8.8.8.8"), - InetAddress.getByName("8.8.4.4")) - .includeIPv6(false) - .build(); - - final DnsOverHttps cloudflareDns = new DnsOverHttps.Builder() - .client(bootstrap) - .url(HttpUrl.get("https://cloudflare-dns.com/dns-query")) - .bootstrapDnsHosts( - InetAddress.getByName("1.1.1.1"), - InetAddress.getByName("1.0.0.1")) - .includeIPv6(false) - .build(); - - builder.dns(hostname -> { - try { - List result = googleDns.lookup(hostname); - if (result != null && !result.isEmpty()) { - return result; - } - } catch (Exception ignored) { - } - - try { - List result = cloudflareDns.lookup(hostname); - if (result != null && !result.isEmpty()) { - return result; - } - } catch (Exception ignored) { - } - - return Dns.SYSTEM.lookup(hostname); - }); - - client = builder.build(); - } catch (Exception e) { - client = new OkHttpClient.Builder() - .connectTimeout(15, TimeUnit.SECONDS) - .readTimeout(15, TimeUnit.SECONDS) - .followRedirects(true) - .dns(Dns.SYSTEM) - .build(); - } - CLIENT = client; - } - private StreamUrlResolver() { } @@ -306,7 +208,12 @@ public final class StreamUrlResolver { return null; } - Collections.sort(pairs, Comparator.comparingInt(pair -> pair.index)); + Collections.sort(pairs, new Comparator() { + @Override + public int compare(EncodedPair left, EncodedPair right) { + return Integer.compare(left.index, right.index); + } + }); StringBuilder decoded = new StringBuilder(pairs.size()); for (EncodedPair pair : pairs) { @@ -357,10 +264,10 @@ public final class StreamUrlResolver { if (url == null || url.isEmpty()) { return false; } - String lower = url.toLowerCase(); + String lower = url.toLowerCase(Locale.ROOT); return lower.contains(".m3u8") || lower.contains(".mpd") || - lower.contains("stream") && lower.contains(".php") == false || + (lower.contains("stream") && !lower.contains(".php")) || lower.endsWith(".mp4") || lower.endsWith(".ts"); } @@ -390,36 +297,16 @@ public final class StreamUrlResolver { return null; } - /** - * Intenta determinar el tipo de contenido del HTML - */ - private static String getContentTypeFromHtml(String html) { - if (html == null || html.isEmpty()) { - return "unknown"; - } - String trimmed = html.trim(); - if (trimmed.startsWith("#EXTM3U") || trimmed.startsWith("#EXT")) { - return "m3u8"; - } - if (trimmed.startsWith("{") || trimmed.startsWith("[")) { - return "json"; - } - if (trimmed.startsWith("<")) { - return "html"; - } - return "text"; - } - private static String downloadPage(String pageUrl) throws IOException { Request request = new Request.Builder() .url(pageUrl) - .header("User-Agent", USER_AGENT) + .header("User-Agent", NetworkUtils.getUserAgent()) .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") .header("Accept-Language", "es-ES,es;q=0.9,en;q=0.8") .header("Referer", "http://streamtp10.com/") .build(); - try (Response response = CLIENT.newCall(request).execute()) { + try (Response response = NetworkUtils.getClient().newCall(request).execute()) { if (!response.isSuccessful()) { throw new IOException("Error HTTP " + response.code() + " al cargar la página del stream"); } diff --git a/app/src/main/java/com/streamplayer/UpdateManager.java b/app/src/main/java/com/streamplayer/UpdateManager.java index 276d889..2bce6de 100644 --- a/app/src/main/java/com/streamplayer/UpdateManager.java +++ b/app/src/main/java/com/streamplayer/UpdateManager.java @@ -20,6 +20,7 @@ import android.util.Log; import android.widget.Toast; import androidx.core.content.FileProvider; +import androidx.core.content.ContextCompat; import org.json.JSONArray; import org.json.JSONException; @@ -34,7 +35,6 @@ import java.util.concurrent.Executors; import okhttp3.OkHttpClient; import okhttp3.Request; -import okhttp3.Request; import okhttp3.Response; /** @@ -117,7 +117,7 @@ public class UpdateManager { return; } File targetDir = appContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS); - if (targetDir == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (targetDir == null) { targetDir = appContext.getExternalFilesDir(null); } if (targetDir == null) { @@ -273,21 +273,27 @@ public class UpdateManager { if (assets == null) { return null; } - JSONObject fallback = null; + JSONObject firstApk = null; + JSONObject debugApk = 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; + if (name.contains("release")) { + return asset; + } + if (firstApk == null) { + firstApk = asset; + } + if (name.contains("debug") && debugApk == null) { + debugApk = asset; + } } } - return fallback; + return firstApk != null ? firstApk : debugApk; } private String deriveVersionName(String tagName, String fallback) { @@ -363,7 +369,12 @@ public class UpdateManager { } downloadReceiver = new DownloadReceiver(); IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE); - appContext.registerReceiver(downloadReceiver, filter); + ContextCompat.registerReceiver( + appContext, + downloadReceiver, + filter, + ContextCompat.RECEIVER_NOT_EXPORTED + ); } private void unregisterDownloadReceiver() { @@ -440,7 +451,10 @@ public class UpdateManager { if (callback == null) { return; } - mainHandler.post(() -> callback.onError(message)); + String safeMessage = TextUtils.isEmpty(message) + ? appContext.getString(R.string.update_error_unknown) + : message; + mainHandler.post(() -> callback.onError(safeMessage)); } private void showToast(String message) { @@ -533,4 +547,3 @@ public class UpdateManager { } } } - diff --git a/app/src/main/java/com/streamplayer/VlcDrmManager.java b/app/src/main/java/com/streamplayer/VlcDrmManager.java deleted file mode 100644 index 083fe26..0000000 --- a/app/src/main/java/com/streamplayer/VlcDrmManager.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.streamplayer; - -import org.videolan.libvlc.Media; - -import java.util.Map; - -/** - * Handles DRM configuration for VLC Media Player - * Supports ClearKey DRM for protected streaming services - */ -public class VlcDrmManager { - - // ClearKey DRM configuration - private static final String CLEARKEY_KEY_SYSTEM = "org.w3.clearkey"; - - /** - * Configure ClearKey DRM for a Media object - * @param media The VLC Media object - * @param keyId The key ID (extracted from manifest or license server) - * @param key The content key - */ - public static void configureClearKey(Media media, String keyId, String key) { - if (media == null || keyId == null || key == null) { - return; - } - - // VLC uses a specific format for ClearKey - // Format: keyid:key - String keyPair = keyId + ":" + key; - media.addOption(":demux=avformat"); - media.addOption(":key-format=" + CLEARKEY_KEY_SYSTEM); - media.addOption(":key=" + keyPair); - } - - /** - * Configure ClearKey DRM with multiple keys - * @param media The VLC Media object - * @param keys Map of keyId -> key pairs - */ - public static void configureClearKey(Media media, Map keys) { - if (media == null || keys == null || keys.isEmpty()) { - return; - } - - media.addOption(":demux=avformat"); - media.addOption(":key-format=" + CLEARKEY_KEY_SYSTEM); - - for (Map.Entry entry : keys.entrySet()) { - String keyPair = entry.getKey() + ":" + entry.getValue(); - media.addOption(":key=" + keyPair); - } - } - - /** - * Configure Widevine DRM (for reference, if needed later) - * VLC supports Widevine via specific options - */ - public static void configureWidevine(Media media, String drmServerUrl) { - if (media == null || drmServerUrl == null) { - return; - } - - media.addOption(":drm=widevine"); - media.addOption(":aes-key=" + drmServerUrl); - } -} diff --git a/app/src/main/java/com/streamplayer/VlcPlayerConfig.java b/app/src/main/java/com/streamplayer/VlcPlayerConfig.java index a3851b7..a703c4e 100644 --- a/app/src/main/java/com/streamplayer/VlcPlayerConfig.java +++ b/app/src/main/java/com/streamplayer/VlcPlayerConfig.java @@ -1,29 +1,14 @@ package com.streamplayer; -/** - * Configuration constants for VLC Player - */ -public class VlcPlayerConfig { - - // Network caching (ms) - public static final int NETWORK_CACHING = 1500; - - // Live streaming caching (ms) - public static final int LIVE_CACHING = 5000; +public final class VlcPlayerConfig { // User Agent public static final String USER_AGENT = "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36"; - // Hardware acceleration - public static final String HW_ACCELERATION = "automatic"; - - // Chroma format - public static final String CHROMA = "RV32"; - - // Audio output - public static final String AUDIO_OUTPUT = "opensles"; - // Maximum retries for playback public static final int MAX_RETRIES = 3; + + private VlcPlayerConfig() { + } } 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_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_player.xml b/app/src/main/res/layout/activity_player.xml index e9ee77f..c0c3c12 100644 --- a/app/src/main/res/layout/activity_player.xml +++ b/app/src/main/res/layout/activity_player.xml @@ -34,7 +34,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" - android:text="Canal" + android:text="@string/player_channel_default" android:textColor="@color/white" android:textSize="18sp" android:textStyle="bold" /> @@ -43,7 +43,7 @@ android:id="@+id/close_button" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Elegir otro" + android:text="@string/player_action_choose_other" android:textAllCaps="false" /> diff --git a/app/src/main/res/layout/item_channel.xml b/app/src/main/res/layout/item_channel.xml index 6c966f6..07d2fe3 100644 --- a/app/src/main/res/layout/item_channel.xml +++ b/app/src/main/res/layout/item_channel.xml @@ -1,5 +1,6 @@ + android:src="@drawable/ic_channel_default" + app:tint="@color/white" /> - - - Azteca Deportes - Canal 5 MX - Caliente TV MX - DAZN 1 - DAZN 2 - DAZN LaLiga - DSports - DSports 2 - DSports Plus - ESPN - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 40ec13f..cacb7b0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,13 +1,15 @@ StreamPlayer Todo el deporte en un solo lugar - Canales Eventos Todos los canales No hay canales disponibles No hay eventos disponibles Actualizar No se pudieron cargar los eventos: %1$s + Canal + Elegir otro + Error de conexión. Reintentando... (%1$d/%2$d) Actualización obligatoria Actualización disponible Actualizar @@ -22,6 +24,7 @@ Novedades Puedes continuar usando la app, pero recomendamos instalar la actualización para obtener el mejor rendimiento. No se pudo verificar actualizaciones (%1$s) + Error desconocido No se pudo abrir el detalle de la versión Respuesta vacía del servidor de releases Error de red (%1$d) @@ -40,7 +43,6 @@ Dispositivo bloqueado Este dispositivo fue bloqueado desde el panel de control. Motivo: %1$s Sin motivo especificado. - Comparte este código con el administrador para solicitar acceso: %1$s Código de verificación Salir Copiar código diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml deleted file mode 100644 index a5156bd..0000000 --- a/app/src/main/res/xml/backup_rules.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml deleted file mode 100644 index d715ae6..0000000 --- a/app/src/main/res/xml/data_extraction_rules.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/build.gradle b/build.gradle index 4426a33..6168297 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,6 @@ allprojects { repositories { google() mavenCentral() - maven { url "https://get.videolan.org/android/maven2/" } } }