549 lines
20 KiB
Java
549 lines
20 KiB
Java
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<EventItem> cachedEvents = new ArrayList<>();
|
|
private List<SectionEntry> 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<EventItem> 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<SectionEntry> buildSections() {
|
|
List<SectionEntry> list = new ArrayList<>();
|
|
list.add(SectionEntry.events(getString(R.string.section_events)));
|
|
|
|
Map<String, List<StreamChannel>> grouped = new HashMap<>();
|
|
List<StreamChannel> allChannels = ChannelRepository.getChannels();
|
|
for (StreamChannel channel : allChannels) {
|
|
String key = deriveGroupName(channel.getName());
|
|
grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(channel);
|
|
}
|
|
|
|
List<StreamChannel> espnChannels = grouped.remove("ESPN");
|
|
if (espnChannels != null && !espnChannels.isEmpty()) {
|
|
list.add(SectionEntry.channels("ESPN", espnChannels));
|
|
}
|
|
|
|
List<String> remaining = new ArrayList<>(grouped.keySet());
|
|
Collections.sort(remaining, String.CASE_INSENSITIVE_ORDER);
|
|
for (String key : remaining) {
|
|
List<StreamChannel> 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<String> getSectionTitles() {
|
|
List<String> 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<StreamChannel> channels;
|
|
|
|
private SectionEntry(String title, Type type, List<StreamChannel> 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<StreamChannel> channels) {
|
|
return new SectionEntry(title, Type.CHANNELS, channels);
|
|
}
|
|
}
|
|
}
|