diff --git a/app/build.gradle b/app/build.gradle index a327a5d..4bff9ca 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,6 +40,7 @@ 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' diff --git a/app/build/outputs/apk/release/app-release.apk b/app/build/outputs/apk/release/app-release.apk deleted file mode 100644 index 53c234a..0000000 Binary files a/app/build/outputs/apk/release/app-release.apk and /dev/null differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 65a6079..a947d5c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,12 +15,17 @@ android:theme="@style/Theme.StreamPlayer" android:usesCleartextTraffic="true"> + + + android:exported="true"> + diff --git a/app/src/main/java/com/streamplayer/ChannelAdapter.java b/app/src/main/java/com/streamplayer/ChannelAdapter.java new file mode 100644 index 0000000..3f348ef --- /dev/null +++ b/app/src/main/java/com/streamplayer/ChannelAdapter.java @@ -0,0 +1,63 @@ +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.List; + +public class ChannelAdapter extends RecyclerView.Adapter { + + public interface OnChannelClickListener { + void onChannelClick(StreamChannel channel); + } + + private final List channels; + private final OnChannelClickListener listener; + + public ChannelAdapter(List channels, OnChannelClickListener listener) { + this.channels = channels; + 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); + } + }); + } + + @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); + } + } +} diff --git a/app/src/main/java/com/streamplayer/ChannelRepository.java b/app/src/main/java/com/streamplayer/ChannelRepository.java new file mode 100644 index 0000000..bfeec39 --- /dev/null +++ b/app/src/main/java/com/streamplayer/ChannelRepository.java @@ -0,0 +1,84 @@ +package com.streamplayer; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public final class ChannelRepository { + + private static final List CHANNELS = Collections.unmodifiableList(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") + )); + + private ChannelRepository() { + } + + public static List getChannels() { + return CHANNELS; + } +} diff --git a/app/src/main/java/com/streamplayer/MainActivity.java b/app/src/main/java/com/streamplayer/MainActivity.java index d3a92d5..a837062 100644 --- a/app/src/main/java/com/streamplayer/MainActivity.java +++ b/app/src/main/java/com/streamplayer/MainActivity.java @@ -1,157 +1,29 @@ package com.streamplayer; +import android.content.Intent; import android.os.Bundle; -import android.os.StrictMode; -import android.view.View; -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.ui.PlayerView; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; public class MainActivity extends AppCompatActivity { - private ExoPlayer player; - private PlayerView playerView; - private ProgressBar loadingIndicator; - private TextView errorMessage; - - private static final String STREAM_PAGE_URL = "https://streamtpmedia.com/global2.php?stream=espn"; - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - - // Configurar política de red para allow cleartext traffic - StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build(); - StrictMode.setThreadPolicy(policy); - setContentView(R.layout.activity_main); - initViews(); - - // Configurar DNS de Google para streaming - DNSSetter.configureDNSToGoogle(this); - - initializePlayer(); - } - - private void initViews() { - playerView = findViewById(R.id.player_view); - loadingIndicator = findViewById(R.id.loading_indicator); - errorMessage = findViewById(R.id.error_message); - } - - private void initializePlayer() { - showLoading(true); - new Thread(() -> { - try { - String resolvedUrl = StreamUrlResolver.resolve(STREAM_PAGE_URL); - runOnUiThread(() -> startPlayback(resolvedUrl)); - } catch (Exception e) { - runOnUiThread(() -> showError("Error al obtener stream: " + e.getMessage())); - } - }).start(); - } - - private void startPlayback(String streamUrl) { - try { - releasePlayer(); - player = new ExoPlayer.Builder(this).build(); - playerView.setPlayer(player); - - player.addListener(new Player.Listener() { - @Override - public void onPlaybackStateChanged(int playbackState) { - switch (playbackState) { - case Player.STATE_BUFFERING: - showLoading(true); - break; - case Player.STATE_READY: - showLoading(false); - break; - case Player.STATE_ENDED: - // Video terminado - break; - } - } - - @Override - public void onPlayerError(PlaybackException error) { - showError("Error al reproducir: " + error.getMessage()); - } - }); - - MediaItem mediaItem = MediaItem.fromUri(streamUrl); - player.setMediaItem(mediaItem); - player.prepare(); - player.setPlayWhenReady(true); - - } 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); - } - - private void showError(String message) { - loadingIndicator.setVisibility(View.GONE); - playerView.setVisibility(View.GONE); - errorMessage.setVisibility(View.VISIBLE); - errorMessage.setText(message); - } - - @Override - protected void onStart() { - super.onStart(); - if (player != null) { - playerView.onResume(); - } - } - - @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(); - if (player != null) { - playerView.onPause(); - } - } - - @Override - protected void onDestroy() { - super.onDestroy(); - releasePlayer(); - } - - private void releasePlayer() { - if (player != null) { - player.release(); - player = null; - } + RecyclerView recyclerView = findViewById(R.id.channel_grid); + recyclerView.setLayoutManager(new GridLayoutManager(this, 3)); + ChannelAdapter adapter = new ChannelAdapter( + ChannelRepository.getChannels(), + channel -> { + Intent intent = new Intent(MainActivity.this, PlayerActivity.class); + intent.putExtra(PlayerActivity.EXTRA_CHANNEL_NAME, channel.getName()); + intent.putExtra(PlayerActivity.EXTRA_CHANNEL_URL, channel.getPageUrl()); + startActivity(intent); + }); + recyclerView.setAdapter(adapter); } } diff --git a/app/src/main/java/com/streamplayer/PlayerActivity.java b/app/src/main/java/com/streamplayer/PlayerActivity.java new file mode 100644 index 0000000..f439fea --- /dev/null +++ b/app/src/main/java/com/streamplayer/PlayerActivity.java @@ -0,0 +1,203 @@ +package com.streamplayer; + +import android.content.Intent; +import android.os.Bundle; +import android.os.StrictMode; +import android.view.View; +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.ui.PlayerView; + +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; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + StrictMode.setThreadPolicy( + new StrictMode.ThreadPolicy.Builder().permitAll().build() + ); + + setContentView(R.layout.activity_player); + + 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(); + player = new ExoPlayer.Builder(this).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) { + showError("Error al reproducir: " + error.getMessage()); + } + }); + + MediaItem mediaItem = MediaItem.fromUri(streamUrl); + player.setMediaItem(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; + } + } + + @Override + protected void onStart() { + super.onStart(); + if (player != null) { + playerView.onResume(); + } + } + + @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(); + if (player != null) { + playerView.onPause(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + releasePlayer(); + } + + 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/StreamChannel.java b/app/src/main/java/com/streamplayer/StreamChannel.java new file mode 100644 index 0000000..beb6025 --- /dev/null +++ b/app/src/main/java/com/streamplayer/StreamChannel.java @@ -0,0 +1,19 @@ +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/res/drawable/bg_channel_item.xml b/app/src/main/res/drawable/bg_channel_item.xml new file mode 100644 index 0000000..359e8fd --- /dev/null +++ b/app/src/main/res/drawable/bg_channel_item.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_channel_default.xml b/app/src/main/res/drawable/ic_channel_default.xml new file mode 100644 index 0000000..4d3663b --- /dev/null +++ b/app/src/main/res/drawable/ic_channel_default.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 2b6b475..c09eba1 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -7,35 +7,32 @@ android:background="@color/black" tools:context=".MainActivity"> - - - - + + diff --git a/app/src/main/res/layout/activity_player.xml b/app/src/main/res/layout/activity_player.xml new file mode 100644 index 0000000..1dc7f3f --- /dev/null +++ b/app/src/main/res/layout/activity_player.xml @@ -0,0 +1,73 @@ + + + + + + + + + +