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.net.Uri; import android.net.VpnService; import android.os.Bundle; import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import android.text.TextUtils; import androidx.annotation.NonNull; 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; public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; private RecyclerView sectionList; private RecyclerView contentList; private ProgressBar loadingIndicator; private TextView messageView; private TextView contentTitle; private Button refreshButton; private Button vpnButton; 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; @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); refreshButton = findViewById(R.id.refresh_button); vpnButton = findViewById(R.id.vpn_button); VpnManager.init(this); updateVpnButton(); vpnButton.setOnClickListener(v -> toggleVpn()); refreshButton.setOnClickListener(v -> { loadEvents(true); Toast.makeText(this, "Actualizando eventos...", Toast.LENGTH_SHORT).show(); }); 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) { @Override public View onInterceptFocusSearch(View focused, int direction) { if (direction == View.FOCUS_DOWN) { int pos = getPosition(focused); if (pos == getItemCount() - 1) { return focused; } } return super.onInterceptFocusSearch(focused, direction); } }; 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(); } } }); } @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(); } } 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) { contentTitle.setText(section.title); refreshButton.setVisibility(View.GONE); contentList.setLayoutManager(channelLayoutManager); contentList.setAdapter(channelAdapter); // Clear any scroll listeners from Events section contentList.clearOnScrollListeners(); 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() { contentTitle.setText(currentSection != null ? currentSection.title : getString(R.string.section_events)); refreshButton.setVisibility(View.VISIBLE); contentList.setLayoutManager(eventLayoutManager); contentList.setAdapter(eventAdapter); // Clear existing listeners contentList.clearOnScrollListeners(); if (cachedEvents.isEmpty()) { loadEvents(false); } else { displayEvents(); } } private void loadEvents(boolean forceRefresh) { 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); } }); } @Override public void onError(String message) { runOnUiThread(() -> { loadingIndicator.setVisibility(View.GONE); messageView.setVisibility(View.VISIBLE); messageView.setText(getString(R.string.message_events_error, message)); }); } }); } 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; } boolean forceUpdate = info.isMandatory(BuildConfig.VERSION_CODE); showUpdateDialog(info, forceUpdate); } private void showUpdateDialog(UpdateManager.UpdateInfo info, boolean mandatory) { if (isFinishing()) { return; } if (updateDialog != null && updateDialog.isShowing()) { updateDialog.dismiss(); } AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.ThemeOverlay_StreamPlayer_AlertDialog) .setTitle(mandatory ? R.string.update_required_title : R.string.update_available_title) .setMessage(buildUpdateMessage(info)) .setPositiveButton(R.string.update_action_download, (dialog, which) -> updateManager.downloadUpdate(MainActivity.this, info)) .setNeutralButton(R.string.update_action_view_release, (dialog, which) -> openReleasePage(info)); if (mandatory) { builder.setCancelable(false); builder.setNegativeButton(R.string.update_action_close_app, (dialog, which) -> finish()); } else { builder.setNegativeButton(R.string.update_action_later, null); } updateDialog = builder.show(); } 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()); } if (!info.isMandatory(BuildConfig.VERSION_CODE)) { builder.append("\n\n"); builder.append(getString(R.string.update_optional_hint)); } return builder.toString(); } private void openReleasePage(UpdateManager.UpdateInfo info) { String url = info.releasePageUrl; if (url == null || url.isEmpty()) { url = info.downloadUrl; } if (url == null || url.isEmpty()) { Toast.makeText(this, R.string.update_error_missing_url, Toast.LENGTH_SHORT).show(); return; } Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); try { startActivity(intent); } catch (Exception e) { Toast.makeText(this, R.string.update_error_open_release, Toast.LENGTH_SHORT).show(); } } 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) -> finish()); 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 toggleVpn() { VpnManager vpnManager = VpnManager.getInstance(); if (vpnManager.isConnected()) { vpnManager.disconnect(); Toast.makeText(this, R.string.vpn_disconnected, Toast.LENGTH_SHORT).show(); updateVpnButton(); } else { // Check if VPN permission is needed try { Intent prepareIntent = com.wireguard.android.backend.GoBackend.VpnService.prepare(this); if (prepareIntent != null) { // Need permission try { startActivityForResult(prepareIntent, 1001); } catch (Exception e) { Toast.makeText(this, "Error: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } return; } } catch (Exception e) { Log.e(TAG, "Error checking VPN permission: " + e.getMessage()); } vpnButton.setText(R.string.vpn_connecting); vpnButton.setEnabled(false); new Thread(() -> { boolean success = vpnManager.connect(); runOnUiThread(() -> { vpnButton.setEnabled(true); if (success) { Toast.makeText(this, R.string.vpn_connected, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(this, R.string.vpn_error, Toast.LENGTH_SHORT).show(); } updateVpnButton(); }); }).start(); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == 1001 && resultCode == RESULT_OK) { // Permission granted, try to connect toggleVpn(); } } private void updateVpnButton() { VpnManager vpnManager = VpnManager.getInstance(); if (vpnManager.isConnected()) { vpnButton.setText(R.string.vpn_disconnect); vpnButton.setSelected(true); } else { vpnButton.setText(R.string.vpn_connect); vpnButton.setSelected(false); } } 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); } } }