From cf0d39f5243088a70c95899fe4355f193f449c9a Mon Sep 17 00:00:00 2001 From: letradocp Date: Tue, 22 Apr 2025 20:40:11 -0300 Subject: [PATCH] Add files via upload --- .gitattributes | 2 + AddPlaylistFragment.java | 113 +++++++++++ AndroidManifest.xml | 31 +++ Channel.java | 92 +++++++++ ChannelAdapter.java | 125 ++++++++++++ ChannelsFragment.java | 177 +++++++++++++++++ GroupsFragment.java | 112 +++++++++++ M3UInputFragment.java | 69 +++++++ M3UParser.java | 148 ++++++++++++++ MainActivity.java | 168 ++++++++++++++++ PlayerFragment.java | 275 ++++++++++++++++++++++++++ Playlist.java | 136 +++++++++++++ PlaylistManager.java | 314 ++++++++++++++++++++++++++++++ PlaylistTabAdapter.java | 33 ++++ README.md | 90 ++++++++- XtreamCodesClient.java | 115 +++++++++++ XtreamInputFragment.java | 80 ++++++++ activity_main.xml | 47 +++++ activity_main_drawer.xml | 42 ++++ build.gradle | 63 ++++-- build_instructions.md | 92 +++++++++ colors.xml | 14 ++ default_channel_logo.xml | 16 ++ fragment_add_playlist.xml | 41 ++++ fragment_channels.xml | 34 ++++ fragment_groups.xml | 26 +++ fragment_m3u_input.xml | 61 ++++++ fragment_player.xml | 126 ++++++++++++ fragment_xtream_input.xml | 89 +++++++++ gradle-wrapper.properties | 6 + ic_favorite.xml | 10 + ic_favorite_border.xml | 13 ++ ic_fullscreen.xml | 10 + ic_guide.xml | 10 + ic_launcher.xml | 5 + ic_launcher_background.xml | 4 + ic_launcher_foreground.xml | 13 ++ ic_launcher_round.xml | 5 + ic_live_tv.xml | 10 + ic_movie.xml | 10 + ic_next.xml | 10 + ic_pause.xml | 10 + ic_play.xml | 10 + ic_playlist_add.xml | 10 + ic_previous.xml | 10 + ic_series.xml | 10 + ic_settings.xml | 10 + item_channel.xml | 53 +++++ nav_header_main.xml | 34 ++++ pitv_source.zip | Bin 0 -> 45305 bytes pitv_source_with_instructions.zip | Bin 0 -> 47114 bytes search_background.xml | 6 + strings.xml | 44 +++++ styles.xml | 28 +++ todo.md | 32 +++ xciptv_research.md | 105 ++++++++++ 56 files changed, 3170 insertions(+), 29 deletions(-) create mode 100644 .gitattributes create mode 100644 AddPlaylistFragment.java create mode 100644 AndroidManifest.xml create mode 100644 Channel.java create mode 100644 ChannelAdapter.java create mode 100644 ChannelsFragment.java create mode 100644 GroupsFragment.java create mode 100644 M3UInputFragment.java create mode 100644 M3UParser.java create mode 100644 MainActivity.java create mode 100644 PlayerFragment.java create mode 100644 Playlist.java create mode 100644 PlaylistManager.java create mode 100644 PlaylistTabAdapter.java create mode 100644 XtreamCodesClient.java create mode 100644 XtreamInputFragment.java create mode 100644 activity_main.xml create mode 100644 activity_main_drawer.xml create mode 100644 build_instructions.md create mode 100644 colors.xml create mode 100644 default_channel_logo.xml create mode 100644 fragment_add_playlist.xml create mode 100644 fragment_channels.xml create mode 100644 fragment_groups.xml create mode 100644 fragment_m3u_input.xml create mode 100644 fragment_player.xml create mode 100644 fragment_xtream_input.xml create mode 100644 gradle-wrapper.properties create mode 100644 ic_favorite.xml create mode 100644 ic_favorite_border.xml create mode 100644 ic_fullscreen.xml create mode 100644 ic_guide.xml create mode 100644 ic_launcher.xml create mode 100644 ic_launcher_background.xml create mode 100644 ic_launcher_foreground.xml create mode 100644 ic_launcher_round.xml create mode 100644 ic_live_tv.xml create mode 100644 ic_movie.xml create mode 100644 ic_next.xml create mode 100644 ic_pause.xml create mode 100644 ic_play.xml create mode 100644 ic_playlist_add.xml create mode 100644 ic_previous.xml create mode 100644 ic_series.xml create mode 100644 ic_settings.xml create mode 100644 item_channel.xml create mode 100644 nav_header_main.xml create mode 100644 pitv_source.zip create mode 100644 pitv_source_with_instructions.zip create mode 100644 search_background.xml create mode 100644 strings.xml create mode 100644 styles.xml create mode 100644 todo.md create mode 100644 xciptv_research.md diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/AddPlaylistFragment.java b/AddPlaylistFragment.java new file mode 100644 index 0000000..d082f6a --- /dev/null +++ b/AddPlaylistFragment.java @@ -0,0 +1,113 @@ +package com.pitv.player.ui; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.viewpager2.widget.ViewPager2; + +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; +import com.pitv.player.R; +import com.pitv.player.adapter.PlaylistTabAdapter; +import com.pitv.player.model.Playlist; +import com.pitv.player.service.PlaylistManager; + +/** + * Fragment para adicionar uma nova playlist + */ +public class AddPlaylistFragment extends Fragment implements + M3UInputFragment.OnM3UAddListener, + XtreamInputFragment.OnXtreamAddListener { + + private TabLayout tabLayout; + private ViewPager2 viewPager; + private PlaylistTabAdapter tabAdapter; + private PlaylistManager playlistManager; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + playlistManager = PlaylistManager.getInstance(requireContext()); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_add_playlist, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + tabLayout = view.findViewById(R.id.tab_layout); + viewPager = view.findViewById(R.id.view_pager); + + setupViewPager(); + } + + /** + * Configura o ViewPager com os fragmentos de entrada M3U e Xtream Codes + */ + private void setupViewPager() { + tabAdapter = new PlaylistTabAdapter(this); + viewPager.setAdapter(tabAdapter); + + // Conecta o TabLayout com o ViewPager2 + new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> { + if (position == 0) { + tab.setText(R.string.m3u_url); + } else { + tab.setText(R.string.xtream_codes); + } + }).attach(); + } + + @Override + public void onM3UPlaylistAdd(String name, String url) { + playlistManager.addM3UPlaylist(name, url, new PlaylistManager.PlaylistLoadListener() { + @Override + public void onPlaylistLoaded(Playlist playlist) { + requireActivity().runOnUiThread(() -> { + Toast.makeText(requireContext(), R.string.playlist_added, Toast.LENGTH_SHORT).show(); + // Volta para a lista de canais + requireActivity().getSupportFragmentManager().popBackStack(); + }); + } + + @Override + public void onPlaylistLoadFailed(Exception e) { + requireActivity().runOnUiThread(() -> { + Toast.makeText(requireContext(), R.string.error_loading, Toast.LENGTH_SHORT).show(); + }); + } + }); + } + + @Override + public void onXtreamPlaylistAdd(String name, String url, String username, String password) { + playlistManager.addXtreamCodesPlaylist(name, url, username, password, new PlaylistManager.PlaylistLoadListener() { + @Override + public void onPlaylistLoaded(Playlist playlist) { + requireActivity().runOnUiThread(() -> { + Toast.makeText(requireContext(), R.string.playlist_added, Toast.LENGTH_SHORT).show(); + // Volta para a lista de canais + requireActivity().getSupportFragmentManager().popBackStack(); + }); + } + + @Override + public void onPlaylistLoadFailed(Exception e) { + requireActivity().runOnUiThread(() -> { + Toast.makeText(requireContext(), R.string.error_loading, Toast.LENGTH_SHORT).show(); + }); + } + }); + } +} diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 0000000..c6c0eb9 --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Channel.java b/Channel.java new file mode 100644 index 0000000..9c097ba --- /dev/null +++ b/Channel.java @@ -0,0 +1,92 @@ +package com.pitv.player.model; + +import java.io.Serializable; + +/** + * Classe que representa um canal de IPTV + */ +public class Channel implements Serializable { + private String id; + private String name; + private String url; + private String logoUrl; + private String group; + private boolean isFavorite; + + public Channel() { + // Construtor vazio necessário para algumas operações + } + + public Channel(String id, String name, String url, String logoUrl, String group) { + this.id = id; + this.name = name; + this.url = url; + this.logoUrl = logoUrl; + this.group = group; + this.isFavorite = false; + } + + // Getters e Setters + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getLogoUrl() { + return logoUrl; + } + + public void setLogoUrl(String logoUrl) { + this.logoUrl = logoUrl; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public boolean isFavorite() { + return isFavorite; + } + + public void setFavorite(boolean favorite) { + isFavorite = favorite; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Channel channel = (Channel) o; + + return id != null ? id.equals(channel.id) : channel.id == null; + } + + @Override + public int hashCode() { + return id != null ? id.hashCode() : 0; + } +} diff --git a/ChannelAdapter.java b/ChannelAdapter.java new file mode 100644 index 0000000..f4eda4e --- /dev/null +++ b/ChannelAdapter.java @@ -0,0 +1,125 @@ +package com.pitv.player.adapter; + +import android.content.Context; +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 com.bumptech.glide.Glide; +import com.pitv.player.R; +import com.pitv.player.model.Channel; +import com.pitv.player.service.PlaylistManager; + +import java.util.List; + +/** + * Adapter para exibição de canais em um RecyclerView + */ +public class ChannelAdapter extends RecyclerView.Adapter { + + private final Context context; + private final List channels; + private final OnChannelClickListener listener; + private final PlaylistManager playlistManager; + + public interface OnChannelClickListener { + void onChannelClick(Channel channel); + } + + public ChannelAdapter(Context context, List channels, OnChannelClickListener listener) { + this.context = context; + this.channels = channels; + this.listener = listener; + this.playlistManager = PlaylistManager.getInstance(context); + } + + @NonNull + @Override + public ChannelViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(context).inflate(R.layout.item_channel, parent, false); + return new ChannelViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ChannelViewHolder holder, int position) { + Channel channel = channels.get(position); + holder.bind(channel); + } + + @Override + public int getItemCount() { + return channels.size(); + } + + class ChannelViewHolder extends RecyclerView.ViewHolder { + private final ImageView logoImageView; + private final TextView nameTextView; + private final TextView groupTextView; + private final ImageView favoriteImageView; + + public ChannelViewHolder(@NonNull View itemView) { + super(itemView); + logoImageView = itemView.findViewById(R.id.channel_logo); + nameTextView = itemView.findViewById(R.id.channel_name); + groupTextView = itemView.findViewById(R.id.channel_group); + favoriteImageView = itemView.findViewById(R.id.favorite_icon); + + itemView.setOnClickListener(v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION && listener != null) { + listener.onChannelClick(channels.get(position)); + } + }); + + favoriteImageView.setOnClickListener(v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + Channel channel = channels.get(position); + toggleFavorite(channel); + notifyItemChanged(position); + } + }); + } + + public void bind(Channel channel) { + nameTextView.setText(channel.getName()); + groupTextView.setText(channel.getGroup()); + + // Carrega o logo do canal + if (channel.getLogoUrl() != null && !channel.getLogoUrl().isEmpty()) { + Glide.with(context) + .load(channel.getLogoUrl()) + .placeholder(R.drawable.default_channel_logo) + .error(R.drawable.default_channel_logo) + .into(logoImageView); + } else { + logoImageView.setImageResource(R.drawable.default_channel_logo); + } + + // Atualiza o ícone de favorito + updateFavoriteIcon(channel); + } + + private void updateFavoriteIcon(Channel channel) { + if (playlistManager.isFavorite(channel)) { + favoriteImageView.setImageResource(R.drawable.ic_favorite); + } else { + favoriteImageView.setImageResource(R.drawable.ic_favorite_border); + } + } + + private void toggleFavorite(Channel channel) { + if (playlistManager.isFavorite(channel)) { + playlistManager.removeFromFavorites(channel); + } else { + playlistManager.addToFavorites(channel); + } + updateFavoriteIcon(channel); + } + } +} diff --git a/ChannelsFragment.java b/ChannelsFragment.java new file mode 100644 index 0000000..b138583 --- /dev/null +++ b/ChannelsFragment.java @@ -0,0 +1,177 @@ +package com.pitv.player.ui; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.SearchView; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.pitv.player.R; +import com.pitv.player.adapter.ChannelAdapter; +import com.pitv.player.model.Channel; +import com.pitv.player.model.Playlist; +import com.pitv.player.service.PlaylistManager; + +import java.util.ArrayList; +import java.util.List; + +/** + * Fragment para exibição da lista de canais + */ +public class ChannelsFragment extends Fragment implements ChannelAdapter.OnChannelClickListener { + private static final String ARG_PLAYLIST_ID = "playlist_id"; + private static final String ARG_GROUP = "group"; + + private String playlistId; + private String group; + + private RecyclerView recyclerView; + private TextView emptyView; + private SearchView searchView; + private ChannelAdapter adapter; + + private PlaylistManager playlistManager; + private List channels = new ArrayList<>(); + + public static ChannelsFragment newInstance(String playlistId, String group) { + ChannelsFragment fragment = new ChannelsFragment(); + Bundle args = new Bundle(); + args.putString(ARG_PLAYLIST_ID, playlistId); + args.putString(ARG_GROUP, group); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (getArguments() != null) { + playlistId = getArguments().getString(ARG_PLAYLIST_ID); + group = getArguments().getString(ARG_GROUP); + } + + playlistManager = PlaylistManager.getInstance(requireContext()); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_channels, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + recyclerView = view.findViewById(R.id.channels_recycler_view); + emptyView = view.findViewById(R.id.empty_view); + searchView = view.findViewById(R.id.search_view); + + // Configura o RecyclerView + recyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + adapter = new ChannelAdapter(requireContext(), channels, this); + recyclerView.setAdapter(adapter); + + // Configura a busca + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + filterChannels(query); + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + filterChannels(newText); + return true; + } + }); + + // Carrega os canais + loadChannels(); + } + + /** + * Carrega os canais da playlist + */ + private void loadChannels() { + channels.clear(); + + if (playlistId != null) { + // Carrega canais de uma playlist específica + Playlist playlist = playlistManager.getPlaylistById(playlistId); + if (playlist != null) { + for (Channel channel : playlist.getChannels()) { + // Se um grupo foi especificado, filtra por ele + if (group == null || group.isEmpty() || channel.getGroup().equals(group)) { + channels.add(channel); + } + } + } + } else { + // Carrega canais favoritos + channels.addAll(playlistManager.getFavorites()); + } + + updateUI(); + } + + /** + * Filtra os canais com base na consulta de busca + * @param query Texto de busca + */ + private void filterChannels(String query) { + List filteredChannels = new ArrayList<>(); + + if (query == null || query.isEmpty()) { + // Se a consulta estiver vazia, mostra todos os canais + loadChannels(); + return; + } + + // Filtra os canais que contêm a consulta no nome + String lowerCaseQuery = query.toLowerCase(); + for (Channel channel : channels) { + if (channel.getName().toLowerCase().contains(lowerCaseQuery)) { + filteredChannels.add(channel); + } + } + + channels.clear(); + channels.addAll(filteredChannels); + updateUI(); + } + + /** + * Atualiza a interface com base nos canais carregados + */ + private void updateUI() { + adapter.notifyDataSetChanged(); + + if (channels.isEmpty()) { + recyclerView.setVisibility(View.GONE); + emptyView.setVisibility(View.VISIBLE); + } else { + recyclerView.setVisibility(View.VISIBLE); + emptyView.setVisibility(View.GONE); + } + } + + @Override + public void onChannelClick(Channel channel) { + // Abre o player com o canal selecionado + PlayerFragment playerFragment = PlayerFragment.newInstance(channel); + requireActivity().getSupportFragmentManager() + .beginTransaction() + .replace(R.id.content_frame, playerFragment) + .addToBackStack(null) + .commit(); + } +} diff --git a/GroupsFragment.java b/GroupsFragment.java new file mode 100644 index 0000000..c5d4aca --- /dev/null +++ b/GroupsFragment.java @@ -0,0 +1,112 @@ +package com.pitv.player.ui; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.pitv.player.R; +import com.pitv.player.model.Playlist; +import com.pitv.player.service.PlaylistManager; + +import java.util.ArrayList; +import java.util.List; + +/** + * Fragment para exibição dos grupos de canais de uma playlist + */ +public class GroupsFragment extends Fragment { + private static final String ARG_PLAYLIST_ID = "playlist_id"; + + private String playlistId; + private ListView listView; + private TextView emptyView; + private PlaylistManager playlistManager; + + public static GroupsFragment newInstance(String playlistId) { + GroupsFragment fragment = new GroupsFragment(); + Bundle args = new Bundle(); + args.putString(ARG_PLAYLIST_ID, playlistId); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (getArguments() != null) { + playlistId = getArguments().getString(ARG_PLAYLIST_ID); + } + + playlistManager = PlaylistManager.getInstance(requireContext()); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_groups, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + listView = view.findViewById(R.id.groups_list_view); + emptyView = view.findViewById(R.id.empty_view); + + // Carrega os grupos + loadGroups(); + + // Configura o listener de clique + listView.setOnItemClickListener((parent, view1, position, id) -> { + String group = (String) parent.getItemAtPosition(position); + + // Abre o fragmento de canais filtrado pelo grupo + ChannelsFragment channelsFragment = ChannelsFragment.newInstance(playlistId, group); + requireActivity().getSupportFragmentManager() + .beginTransaction() + .replace(R.id.content_frame, channelsFragment) + .addToBackStack(null) + .commit(); + }); + } + + /** + * Carrega os grupos da playlist + */ + private void loadGroups() { + List groups = new ArrayList<>(); + + if (playlistId != null) { + Playlist playlist = playlistManager.getPlaylistById(playlistId); + if (playlist != null) { + groups.addAll(playlist.getGroups()); + } + } + + // Configura o adapter + ArrayAdapter adapter = new ArrayAdapter<>( + requireContext(), + android.R.layout.simple_list_item_1, + groups + ); + + listView.setAdapter(adapter); + + // Atualiza a visibilidade + if (groups.isEmpty()) { + listView.setVisibility(View.GONE); + emptyView.setVisibility(View.VISIBLE); + } else { + listView.setVisibility(View.VISIBLE); + emptyView.setVisibility(View.GONE); + } + } +} diff --git a/M3UInputFragment.java b/M3UInputFragment.java new file mode 100644 index 0000000..313276d --- /dev/null +++ b/M3UInputFragment.java @@ -0,0 +1,69 @@ +package com.pitv.player.ui; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.pitv.player.R; + +/** + * Fragment para entrada de URL M3U + */ +public class M3UInputFragment extends Fragment { + + private EditText editM3uUrl; + private EditText editPlaylistName; + private Button btnAddPlaylist; + + public interface OnM3UAddListener { + void onM3UPlaylistAdd(String name, String url); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_m3u_input, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + editM3uUrl = view.findViewById(R.id.edit_m3u_url); + editPlaylistName = view.findViewById(R.id.edit_playlist_name); + btnAddPlaylist = view.findViewById(R.id.btn_add_playlist); + + btnAddPlaylist.setOnClickListener(v -> addPlaylist()); + } + + /** + * Adiciona uma nova playlist M3U + */ + private void addPlaylist() { + String url = editM3uUrl.getText().toString().trim(); + String name = editPlaylistName.getText().toString().trim(); + + if (url.isEmpty()) { + Toast.makeText(requireContext(), R.string.invalid_url, Toast.LENGTH_SHORT).show(); + return; + } + + // Se o nome estiver vazio, usa a URL como nome + if (name.isEmpty()) { + name = url; + } + + // Notifica o listener + if (getParentFragment() instanceof OnM3UAddListener) { + ((OnM3UAddListener) getParentFragment()).onM3UPlaylistAdd(name, url); + } + } +} diff --git a/M3UParser.java b/M3UParser.java new file mode 100644 index 0000000..65a5765 --- /dev/null +++ b/M3UParser.java @@ -0,0 +1,148 @@ +package com.pitv.player.parser; + +import com.pitv.player.model.Channel; +import com.pitv.player.model.Playlist; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parser para arquivos M3U + */ +public class M3UParser { + + private static final String EXTM3U = "#EXTM3U"; + private static final String EXTINF = "#EXTINF:"; + private static final String TVG_NAME = "tvg-name=\""; + private static final String TVG_LOGO = "tvg-logo=\""; + private static final String GROUP_TITLE = "group-title=\""; + + /** + * Faz o parse de uma playlist M3U a partir de uma URL + * @param playlist Objeto Playlist a ser preenchido + * @return Playlist preenchida com os canais + */ + public static Playlist parseFromUrl(Playlist playlist) throws IOException { + URL url = new URL(playlist.getUrl()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + + try (InputStream inputStream = connection.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + + return parseFromReader(reader, playlist); + } finally { + connection.disconnect(); + } + } + + /** + * Faz o parse de uma playlist M3U a partir de um BufferedReader + * @param reader BufferedReader com o conteúdo da playlist + * @param playlist Objeto Playlist a ser preenchido + * @return Playlist preenchida com os canais + */ + public static Playlist parseFromReader(BufferedReader reader, Playlist playlist) throws IOException { + String line; + boolean isFirstLine = true; + String channelInfo = ""; + + while ((line = reader.readLine()) != null) { + line = line.trim(); + + // Verifica se é a primeira linha e se começa com #EXTM3U + if (isFirstLine) { + isFirstLine = false; + if (!line.startsWith(EXTM3U)) { + throw new IOException("Formato de arquivo inválido. Não é um arquivo M3U válido."); + } + continue; + } + + // Ignora linhas vazias + if (line.isEmpty()) { + continue; + } + + // Se a linha começa com #EXTINF, é uma informação de canal + if (line.startsWith(EXTINF)) { + channelInfo = line; + } + // Se não começa com # e temos informações de canal, é a URL do canal + else if (!line.startsWith("#") && !channelInfo.isEmpty()) { + Channel channel = parseChannel(channelInfo, line); + playlist.addChannel(channel); + channelInfo = ""; + } + } + + return playlist; + } + + /** + * Faz o parse das informações de um canal + * @param channelInfo Linha com as informações do canal (#EXTINF) + * @param channelUrl URL do canal + * @return Objeto Channel preenchido + */ + private static Channel parseChannel(String channelInfo, String channelUrl) { + String id = UUID.randomUUID().toString(); + String name = extractChannelName(channelInfo); + String logoUrl = extractAttribute(channelInfo, TVG_LOGO); + String group = extractAttribute(channelInfo, GROUP_TITLE); + + // Se não encontrou um grupo, usa "Sem Categoria" + if (group.isEmpty()) { + group = "Sem Categoria"; + } + + return new Channel(id, name, channelUrl, logoUrl, group); + } + + /** + * Extrai o nome do canal da linha de informações + * @param channelInfo Linha com as informações do canal + * @return Nome do canal + */ + private static String extractChannelName(String channelInfo) { + // Tenta extrair do atributo tvg-name primeiro + String name = extractAttribute(channelInfo, TVG_NAME); + + // Se não encontrou, extrai do final da linha + if (name.isEmpty()) { + int commaIndex = channelInfo.lastIndexOf(','); + if (commaIndex != -1 && commaIndex < channelInfo.length() - 1) { + name = channelInfo.substring(commaIndex + 1).trim(); + } else { + name = "Canal Sem Nome"; + } + } + + return name; + } + + /** + * Extrai um atributo da linha de informações do canal + * @param channelInfo Linha com as informações do canal + * @param attribute Atributo a ser extraído (ex: tvg-logo=") + * @return Valor do atributo + */ + private static String extractAttribute(String channelInfo, String attribute) { + int startIndex = channelInfo.indexOf(attribute); + if (startIndex != -1) { + startIndex += attribute.length(); + int endIndex = channelInfo.indexOf("\"", startIndex); + if (endIndex != -1) { + return channelInfo.substring(startIndex, endIndex); + } + } + return ""; + } +} diff --git a/MainActivity.java b/MainActivity.java new file mode 100644 index 0000000..928e5b5 --- /dev/null +++ b/MainActivity.java @@ -0,0 +1,168 @@ +package com.pitv.player; + +import android.os.Bundle; +import android.view.MenuItem; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBarDrawerToggle; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.view.GravityCompat; +import androidx.drawerlayout.widget.DrawerLayout; +import androidx.fragment.app.Fragment; + +import com.google.android.material.navigation.NavigationView; +import com.pitv.player.model.Playlist; +import com.pitv.player.service.PlaylistManager; +import com.pitv.player.ui.AddPlaylistFragment; +import com.pitv.player.ui.ChannelsFragment; + +import java.util.List; + +public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener { + + private DrawerLayout drawerLayout; + private NavigationView navigationView; + private PlaylistManager playlistManager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + drawerLayout = findViewById(R.id.drawer_layout); + navigationView = findViewById(R.id.nav_view); + + // Configura o toggle do drawer + ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( + this, drawerLayout, toolbar, + R.string.navigation_drawer_open, + R.string.navigation_drawer_close); + drawerLayout.addDrawerListener(toggle); + toggle.syncState(); + + // Configura o listener de navegação + navigationView.setNavigationItemSelectedListener(this); + + // Inicializa o gerenciador de playlists + playlistManager = PlaylistManager.getInstance(this); + + // Carrega o fragmento inicial + if (savedInstanceState == null) { + // Se não houver playlists, mostra o fragmento de adicionar playlist + if (playlistManager.getPlaylists().isEmpty()) { + loadFragment(new AddPlaylistFragment()); + navigationView.setCheckedItem(R.id.nav_add_playlist); + } else { + // Caso contrário, mostra a lista de canais + loadFragment(ChannelsFragment.newInstance(null, null)); + navigationView.setCheckedItem(R.id.nav_live_tv); + } + } + + // Atualiza o menu de navegação com as playlists + updateNavigationMenu(); + } + + @Override + protected void onResume() { + super.onResume(); + // Atualiza o menu de navegação quando a atividade é retomada + updateNavigationMenu(); + } + + @Override + public void onBackPressed() { + // Fecha o drawer se estiver aberto + if (drawerLayout.isDrawerOpen(GravityCompat.START)) { + drawerLayout.closeDrawer(GravityCompat.START); + } else { + super.onBackPressed(); + } + } + + @Override + public boolean onNavigationItemSelected(@NonNull MenuItem item) { + int id = item.getItemId(); + + if (id == R.id.nav_live_tv) { + // Mostra todos os canais de todas as playlists + loadFragment(ChannelsFragment.newInstance(null, null)); + } else if (id == R.id.nav_movies) { + // TODO: Implementar visualização de filmes + } else if (id == R.id.nav_series) { + // TODO: Implementar visualização de séries + } else if (id == R.id.nav_favorites) { + // Mostra os canais favoritos + loadFragment(ChannelsFragment.newInstance(null, null)); + } else if (id == R.id.nav_epg) { + // TODO: Implementar guia de programação + } else if (id == R.id.nav_settings) { + // TODO: Implementar configurações + } else if (id == R.id.nav_add_playlist) { + // Mostra o fragmento para adicionar playlist + loadFragment(new AddPlaylistFragment()); + } else { + // Verifica se é um item de playlist dinâmico + String tag = item.getTitle().toString(); + List playlists = playlistManager.getPlaylists(); + + for (Playlist playlist : playlists) { + if (playlist.getName().equals(tag)) { + // Mostra os canais da playlist selecionada + loadFragment(ChannelsFragment.newInstance(playlist.getId(), null)); + break; + } + } + } + + drawerLayout.closeDrawer(GravityCompat.START); + return true; + } + + /** + * Carrega um fragmento no container principal + * @param fragment Fragmento a ser carregado + */ + private void loadFragment(Fragment fragment) { + getSupportFragmentManager() + .beginTransaction() + .replace(R.id.content_frame, fragment) + .commit(); + } + + /** + * Atualiza o menu de navegação com as playlists disponíveis + */ + private void updateNavigationMenu() { + // Obtém o menu de navegação + android.view.Menu menu = navigationView.getMenu(); + + // Remove os itens de playlist existentes + int size = menu.size(); + for (int i = 0; i < size; i++) { + MenuItem item = menu.getItem(i); + if (item.getItemId() > 100) { // IDs dinâmicos para playlists + menu.removeItem(item.getItemId()); + } + } + + // Adiciona as playlists como itens de menu + List playlists = playlistManager.getPlaylists(); + if (!playlists.isEmpty()) { + // Adiciona um submenu para as playlists + android.view.SubMenu playlistsMenu = menu.addSubMenu("Minhas Playlists"); + + int id = 101; // IDs dinâmicos começando de 101 + for (Playlist playlist : playlists) { + playlistsMenu.add(0, id++, 0, playlist.getName()) + .setIcon(R.drawable.ic_playlist_add); + } + } + } +} diff --git a/PlayerFragment.java b/PlayerFragment.java new file mode 100644 index 0000000..b8aeaa8 --- /dev/null +++ b/PlayerFragment.java @@ -0,0 +1,275 @@ +package com.pitv.player.ui; + +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.bumptech.glide.Glide; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.ui.PlayerView; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.util.Util; +import com.pitv.player.R; +import com.pitv.player.model.Channel; +import com.pitv.player.service.PlaylistManager; + +/** + * Fragment para reprodução de vídeo + */ +public class PlayerFragment extends Fragment { + private static final String ARG_CHANNEL = "channel"; + + private Channel channel; + private PlayerView playerView; + private SimpleExoPlayer player; + private ImageView channelLogo; + private TextView channelName; + private TextView programTitle; + private TextView programTime; + private ProgressBar loadingIndicator; + private ImageButton btnPlayPause; + private ImageButton btnFavorite; + + private PlaylistManager playlistManager; + + public static PlayerFragment newInstance(Channel channel) { + PlayerFragment fragment = new PlayerFragment(); + Bundle args = new Bundle(); + args.putSerializable(ARG_CHANNEL, channel); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (getArguments() != null) { + channel = (Channel) getArguments().getSerializable(ARG_CHANNEL); + } + + playlistManager = PlaylistManager.getInstance(requireContext()); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_player, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + playerView = view.findViewById(R.id.player_view); + channelLogo = view.findViewById(R.id.channel_logo); + channelName = view.findViewById(R.id.channel_name); + programTitle = view.findViewById(R.id.program_title); + programTime = view.findViewById(R.id.program_time); + loadingIndicator = view.findViewById(R.id.loading_indicator); + btnPlayPause = view.findViewById(R.id.btn_play_pause); + btnFavorite = view.findViewById(R.id.btn_favorite); + + ImageButton btnPrevious = view.findViewById(R.id.btn_previous); + ImageButton btnNext = view.findViewById(R.id.btn_next); + ImageButton btnFullscreen = view.findViewById(R.id.btn_fullscreen); + + // Configura os botões + btnPlayPause.setOnClickListener(v -> togglePlayPause()); + btnFavorite.setOnClickListener(v -> toggleFavorite()); + btnPrevious.setOnClickListener(v -> playPreviousChannel()); + btnNext.setOnClickListener(v -> playNextChannel()); + btnFullscreen.setOnClickListener(v -> toggleFullscreen()); + + // Configura as informações do canal + updateChannelInfo(); + } + + @Override + public void onStart() { + super.onStart(); + initializePlayer(); + } + + @Override + public void onResume() { + super.onResume(); + if (player == null) { + initializePlayer(); + } + } + + @Override + public void onPause() { + super.onPause(); + releasePlayer(); + } + + @Override + public void onStop() { + super.onStop(); + releasePlayer(); + } + + /** + * Inicializa o player de vídeo + */ + private void initializePlayer() { + if (channel == null) return; + + player = new SimpleExoPlayer.Builder(requireContext()).build(); + playerView.setPlayer(player); + + // Configura o listener de eventos do player + player.addListener(new Player.EventListener() { + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (playbackState == Player.STATE_BUFFERING) { + loadingIndicator.setVisibility(View.VISIBLE); + } else if (playbackState == Player.STATE_READY) { + loadingIndicator.setVisibility(View.GONE); + } + } + + @Override + public void onPlayerError(ExoPlaybackException error) { + loadingIndicator.setVisibility(View.GONE); + // TODO: Mostrar mensagem de erro + } + }); + + // Prepara a fonte de mídia + prepareMediaSource(); + } + + /** + * Prepara a fonte de mídia para reprodução + */ + private void prepareMediaSource() { + Uri uri = Uri.parse(channel.getUrl()); + DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(requireContext(), + Util.getUserAgent(requireContext(), "PITV")); + + MediaSource mediaSource; + if (channel.getUrl().contains(".m3u8")) { + // Fonte HLS + mediaSource = new HlsMediaSource.Factory(dataSourceFactory) + .createMediaSource(uri); + } else { + // Fonte progressiva + mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory) + .createMediaSource(uri); + } + + player.prepare(mediaSource); + player.setPlayWhenReady(true); + } + + /** + * Libera os recursos do player + */ + private void releasePlayer() { + if (player != null) { + player.release(); + player = null; + } + } + + /** + * Atualiza as informações do canal na interface + */ + private void updateChannelInfo() { + if (channel == null) return; + + channelName.setText(channel.getName()); + + // Carrega o logo do canal se disponível + if (channel.getLogoUrl() != null && !channel.getLogoUrl().isEmpty()) { + Glide.with(this) + .load(channel.getLogoUrl()) + .placeholder(R.drawable.default_channel_logo) + .error(R.drawable.default_channel_logo) + .into(channelLogo); + } else { + channelLogo.setImageResource(R.drawable.default_channel_logo); + } + + // Atualiza o ícone de favorito + updateFavoriteIcon(); + + // TODO: Implementar EPG para obter informações do programa atual + programTitle.setText("Informação não disponível"); + programTime.setText(""); + } + + /** + * Atualiza o ícone de favorito com base no status do canal + */ + private void updateFavoriteIcon() { + if (playlistManager.isFavorite(channel)) { + btnFavorite.setImageResource(R.drawable.ic_favorite); + } else { + btnFavorite.setImageResource(R.drawable.ic_favorite_border); + } + } + + /** + * Alterna entre reproduzir e pausar o vídeo + */ + private void togglePlayPause() { + if (player != null) { + player.setPlayWhenReady(!player.getPlayWhenReady()); + btnPlayPause.setImageResource(player.getPlayWhenReady() ? + R.drawable.ic_pause : R.drawable.ic_play); + } + } + + /** + * Alterna o status de favorito do canal + */ + private void toggleFavorite() { + if (playlistManager.isFavorite(channel)) { + playlistManager.removeFromFavorites(channel); + } else { + playlistManager.addToFavorites(channel); + } + updateFavoriteIcon(); + } + + /** + * Reproduz o canal anterior na lista + */ + private void playPreviousChannel() { + // TODO: Implementar navegação entre canais + } + + /** + * Reproduz o próximo canal na lista + */ + private void playNextChannel() { + // TODO: Implementar navegação entre canais + } + + /** + * Alterna entre modo normal e tela cheia + */ + private void toggleFullscreen() { + // TODO: Implementar modo tela cheia + } +} diff --git a/Playlist.java b/Playlist.java new file mode 100644 index 0000000..0c1b785 --- /dev/null +++ b/Playlist.java @@ -0,0 +1,136 @@ +package com.pitv.player.model; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * Classe que representa uma playlist de IPTV + */ +public class Playlist implements Serializable { + private String id; + private String name; + private String url; + private String username; + private String password; + private PlaylistType type; + private List channels; + private List groups; + + public enum PlaylistType { + M3U, + XTREAM_CODES + } + + public Playlist() { + channels = new ArrayList<>(); + groups = new ArrayList<>(); + } + + public Playlist(String id, String name, String url, PlaylistType type) { + this.id = id; + this.name = name; + this.url = url; + this.type = type; + this.channels = new ArrayList<>(); + this.groups = new ArrayList<>(); + } + + public Playlist(String id, String name, String url, String username, String password, PlaylistType type) { + this.id = id; + this.name = name; + this.url = url; + this.username = username; + this.password = password; + this.type = type; + this.channels = new ArrayList<>(); + this.groups = new ArrayList<>(); + } + + // Getters e Setters + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public PlaylistType getType() { + return type; + } + + public void setType(PlaylistType type) { + this.type = type; + } + + public List getChannels() { + return channels; + } + + public void setChannels(List channels) { + this.channels = channels; + } + + public void addChannel(Channel channel) { + this.channels.add(channel); + if (!groups.contains(channel.getGroup())) { + groups.add(channel.getGroup()); + } + } + + public List getGroups() { + return groups; + } + + public void setGroups(List groups) { + this.groups = groups; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Playlist playlist = (Playlist) o; + + return id != null ? id.equals(playlist.id) : playlist.id == null; + } + + @Override + public int hashCode() { + return id != null ? id.hashCode() : 0; + } +} diff --git a/PlaylistManager.java b/PlaylistManager.java new file mode 100644 index 0000000..4d938a5 --- /dev/null +++ b/PlaylistManager.java @@ -0,0 +1,314 @@ +package com.pitv.player.service; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.AsyncTask; +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.pitv.player.api.XtreamCodesClient; +import com.pitv.player.model.Channel; +import com.pitv.player.model.Playlist; +import com.pitv.player.parser.M3UParser; + +import org.json.JSONException; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * Gerenciador de playlists + */ +public class PlaylistManager { + private static final String TAG = "PlaylistManager"; + private static final String PREFS_NAME = "com.pitv.player.playlists"; + private static final String KEY_PLAYLISTS = "playlists"; + private static final String KEY_FAVORITES = "favorites"; + + private static PlaylistManager instance; + private Context context; + private List playlists; + private List favorites; + private PlaylistLoadListener listener; + + public interface PlaylistLoadListener { + void onPlaylistLoaded(Playlist playlist); + void onPlaylistLoadFailed(Exception e); + } + + private PlaylistManager(Context context) { + this.context = context.getApplicationContext(); + this.playlists = loadPlaylistsFromPrefs(); + this.favorites = loadFavoritesFromPrefs(); + } + + public static synchronized PlaylistManager getInstance(Context context) { + if (instance == null) { + instance = new PlaylistManager(context); + } + return instance; + } + + /** + * Adiciona uma nova playlist M3U + * @param name Nome da playlist + * @param url URL da playlist + * @param listener Listener para notificar quando a playlist for carregada + */ + public void addM3UPlaylist(String name, String url, PlaylistLoadListener listener) { + this.listener = listener; + + String id = UUID.randomUUID().toString(); + Playlist playlist = new Playlist(id, name, url, Playlist.PlaylistType.M3U); + + new LoadM3UPlaylistTask().execute(playlist); + } + + /** + * Adiciona uma nova playlist Xtream Codes + * @param name Nome da playlist + * @param url URL do servidor + * @param username Nome de usuário + * @param password Senha + * @param listener Listener para notificar quando a playlist for carregada + */ + public void addXtreamCodesPlaylist(String name, String url, String username, String password, PlaylistLoadListener listener) { + this.listener = listener; + + String id = UUID.randomUUID().toString(); + Playlist playlist = new Playlist(id, name, url, username, password, Playlist.PlaylistType.XTREAM_CODES); + + new LoadXtreamCodesPlaylistTask().execute(playlist); + } + + /** + * Obtém todas as playlists + * @return Lista de playlists + */ + public List getPlaylists() { + return playlists; + } + + /** + * Obtém uma playlist pelo ID + * @param playlistId ID da playlist + * @return Playlist encontrada ou null se não existir + */ + public Playlist getPlaylistById(String playlistId) { + for (Playlist playlist : playlists) { + if (playlist.getId().equals(playlistId)) { + return playlist; + } + } + return null; + } + + /** + * Remove uma playlist + * @param playlistId ID da playlist a ser removida + */ + public void removePlaylist(String playlistId) { + Playlist playlistToRemove = null; + + for (Playlist playlist : playlists) { + if (playlist.getId().equals(playlistId)) { + playlistToRemove = playlist; + break; + } + } + + if (playlistToRemove != null) { + playlists.remove(playlistToRemove); + savePlaylistsToPrefs(); + } + } + + /** + * Adiciona um canal aos favoritos + * @param channel Canal a ser adicionado aos favoritos + */ + public void addToFavorites(Channel channel) { + if (!isFavorite(channel)) { + channel.setFavorite(true); + favorites.add(channel); + saveFavoritesToPrefs(); + } + } + + /** + * Remove um canal dos favoritos + * @param channel Canal a ser removido dos favoritos + */ + public void removeFromFavorites(Channel channel) { + Channel channelToRemove = null; + + for (Channel favoriteChannel : favorites) { + if (favoriteChannel.getId().equals(channel.getId())) { + channelToRemove = favoriteChannel; + break; + } + } + + if (channelToRemove != null) { + channelToRemove.setFavorite(false); + favorites.remove(channelToRemove); + saveFavoritesToPrefs(); + } + } + + /** + * Verifica se um canal está nos favoritos + * @param channel Canal a ser verificado + * @return true se o canal estiver nos favoritos, false caso contrário + */ + public boolean isFavorite(Channel channel) { + for (Channel favoriteChannel : favorites) { + if (favoriteChannel.getId().equals(channel.getId())) { + return true; + } + } + return false; + } + + /** + * Obtém todos os canais favoritos + * @return Lista de canais favoritos + */ + public List getFavorites() { + return favorites; + } + + /** + * Carrega as playlists salvas nas preferências + * @return Lista de playlists + */ + private List loadPlaylistsFromPrefs() { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + String playlistsJson = prefs.getString(KEY_PLAYLISTS, null); + + if (playlistsJson != null) { + Gson gson = new Gson(); + Type type = new TypeToken>(){}.getType(); + return gson.fromJson(playlistsJson, type); + } + + return new ArrayList<>(); + } + + /** + * Salva as playlists nas preferências + */ + private void savePlaylistsToPrefs() { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + + Gson gson = new Gson(); + String playlistsJson = gson.toJson(playlists); + + editor.putString(KEY_PLAYLISTS, playlistsJson); + editor.apply(); + } + + /** + * Carrega os favoritos salvos nas preferências + * @return Lista de canais favoritos + */ + private List loadFavoritesFromPrefs() { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + String favoritesJson = prefs.getString(KEY_FAVORITES, null); + + if (favoritesJson != null) { + Gson gson = new Gson(); + Type type = new TypeToken>(){}.getType(); + return gson.fromJson(favoritesJson, type); + } + + return new ArrayList<>(); + } + + /** + * Salva os favoritos nas preferências + */ + private void saveFavoritesToPrefs() { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + + Gson gson = new Gson(); + String favoritesJson = gson.toJson(favorites); + + editor.putString(KEY_FAVORITES, favoritesJson); + editor.apply(); + } + + /** + * AsyncTask para carregar uma playlist M3U + */ + private class LoadM3UPlaylistTask extends AsyncTask { + private Exception exception; + + @Override + protected Playlist doInBackground(Playlist... playlists) { + Playlist playlist = playlists[0]; + + try { + return M3UParser.parseFromUrl(playlist); + } catch (IOException e) { + Log.e(TAG, "Erro ao carregar playlist M3U", e); + exception = e; + return null; + } + } + + @Override + protected void onPostExecute(Playlist playlist) { + if (playlist != null) { + playlists.add(playlist); + savePlaylistsToPrefs(); + + if (listener != null) { + listener.onPlaylistLoaded(playlist); + } + } else if (exception != null && listener != null) { + listener.onPlaylistLoadFailed(exception); + } + } + } + + /** + * AsyncTask para carregar uma playlist Xtream Codes + */ + private class LoadXtreamCodesPlaylistTask extends AsyncTask { + private Exception exception; + + @Override + protected Playlist doInBackground(Playlist... playlists) { + Playlist playlist = playlists[0]; + + try { + return XtreamCodesClient.loadChannels(playlist); + } catch (IOException | JSONException e) { + Log.e(TAG, "Erro ao carregar playlist Xtream Codes", e); + exception = e; + return null; + } + } + + @Override + protected void onPostExecute(Playlist playlist) { + if (playlist != null) { + playlists.add(playlist); + savePlaylistsToPrefs(); + + if (listener != null) { + listener.onPlaylistLoaded(playlist); + } + } else if (exception != null && listener != null) { + listener.onPlaylistLoadFailed(exception); + } + } + } +} diff --git a/PlaylistTabAdapter.java b/PlaylistTabAdapter.java new file mode 100644 index 0000000..81b7acf --- /dev/null +++ b/PlaylistTabAdapter.java @@ -0,0 +1,33 @@ +package com.pitv.player.adapter; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.viewpager2.adapter.FragmentStateAdapter; + +import com.pitv.player.ui.M3UInputFragment; +import com.pitv.player.ui.XtreamInputFragment; + +/** + * Adapter para as abas de adição de playlist + */ +public class PlaylistTabAdapter extends FragmentStateAdapter { + + public PlaylistTabAdapter(@NonNull Fragment fragment) { + super(fragment); + } + + @NonNull + @Override + public Fragment createFragment(int position) { + if (position == 0) { + return new M3UInputFragment(); + } else { + return new XtreamInputFragment(); + } + } + + @Override + public int getItemCount() { + return 2; // Duas abas: M3U e Xtream Codes + } +} diff --git a/README.md b/README.md index 99a64ef..efe857d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,84 @@ -# Appcircle Sample App for Native (Java/Kotlin) Android Builds -This is a sample Android for testing Android App build functionality in Appcircle. As a Java project, this repository can be used for Android builds. +# Documentação do Aplicativo PITV -## Using the Appcircle Build Module -You can find more information about the Appcircle Build Module and how to connect a Git repository in the [Appcircle Documentation](https://docs.appcircle.io/build/). +## Visão Geral +PITV é um aplicativo Android para reprodução de conteúdo IPTV, similar ao XCIPTV Player. O aplicativo suporta tanto playlists no formato M3U quanto servidores Xtream Codes, permitindo aos usuários assistir a canais de TV ao vivo em seus dispositivos Android. -## Contribution -You can contribute to this repository with fixes and feature updates that do not deviate from the original purpose of the app, which is to demonstrate how the Appcircle Build Module is used. +## Funcionalidades Principais -## Reporting an Issue / Reaching Us -If you encounter any issues related with the app in this repository, you can raise an issue here. -If you have any other issues or questions about Appcircle, you can contact us via our [contact form on Appcircle.io](https://appcircle.io/support) or join our Slack community at [slack.appcircle.io](slack.appcircle.io) +### Gerenciamento de Playlists +- Adição de playlists via URL M3U +- Suporte a servidores Xtream Codes (com autenticação de usuário/senha) +- Gerenciamento de múltiplas playlists + +### Navegação de Conteúdo +- Visualização de canais por categoria/grupo +- Busca de canais por nome +- Lista de favoritos para acesso rápido + +### Reprodução de Mídia +- Player de vídeo integrado com controles de reprodução +- Suporte a streaming HLS e outros formatos +- Opção de tela cheia +- Navegação entre canais + +### Interface do Usuário +- Design moderno e intuitivo +- Menu de navegação lateral +- Visualização de informações do canal +- Suporte a temas escuros + +## Requisitos Técnicos +- Android 5.0 (Lollipop) ou superior +- Conexão com a internet +- Permissões: Internet, Acesso ao Estado da Rede, Armazenamento + +## Instruções de Instalação +1. Baixe o arquivo PITV.apk +2. Habilite a instalação de aplicativos de fontes desconhecidas nas configurações do seu dispositivo +3. Abra o arquivo APK e siga as instruções de instalação +4. Após a instalação, abra o aplicativo PITV + +## Instruções de Uso + +### Adicionar uma Playlist M3U +1. Abra o menu lateral e selecione "Adicionar Playlist" +2. Selecione a aba "URL M3U" +3. Digite um nome para a playlist e a URL do arquivo M3U +4. Toque em "Adicionar" + +### Adicionar um Servidor Xtream Codes +1. Abra o menu lateral e selecione "Adicionar Playlist" +2. Selecione a aba "Xtream Codes" +3. Digite um nome para a playlist, a URL do servidor, nome de usuário e senha +4. Toque em "Adicionar" + +### Assistir a um Canal +1. Selecione uma playlist no menu lateral +2. Navegue pelos grupos de canais ou use a busca para encontrar um canal específico +3. Toque no canal desejado para iniciar a reprodução +4. Use os controles de reprodução para pausar/reproduzir, avançar/retroceder canais ou alternar para tela cheia + +### Adicionar um Canal aos Favoritos +1. Durante a reprodução de um canal, toque no ícone de coração +2. Ou na lista de canais, toque no ícone de coração ao lado do nome do canal +3. Acesse seus canais favoritos através da opção "Favoritos" no menu lateral + +## Solução de Problemas +- Se um canal não reproduzir, verifique sua conexão com a internet +- Certifique-se de que a URL da playlist ou as credenciais do servidor Xtream Codes estão corretas +- Alguns canais podem estar temporariamente indisponíveis devido a problemas no servidor de origem + +## Compilação do Código-Fonte +O código-fonte está organizado seguindo as melhores práticas de desenvolvimento Android: + +- `model`: Classes de modelo de dados (Channel, Playlist) +- `parser`: Parser para arquivos M3U +- `api`: Cliente para API Xtream Codes +- `service`: Gerenciador de playlists e favoritos +- `ui`: Fragmentos e atividades da interface do usuário +- `adapter`: Adaptadores para RecyclerView e ViewPager + +Para compilar o projeto: +1. Importe o código-fonte em Android Studio +2. Sincronize o projeto com os arquivos Gradle +3. Execute a tarefa `assembleDebug` ou `assembleRelease` para gerar o APK diff --git a/XtreamCodesClient.java b/XtreamCodesClient.java new file mode 100644 index 0000000..8400b89 --- /dev/null +++ b/XtreamCodesClient.java @@ -0,0 +1,115 @@ +package com.pitv.player.api; + +import com.pitv.player.model.Channel; +import com.pitv.player.model.Playlist; + +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.util.UUID; + +/** + * Cliente para API Xtream Codes + */ +public class XtreamCodesClient { + + private static final String API_LIVE = "player_api.php?username=%s&password=%s&action=get_live_categories"; + private static final String API_LIVE_STREAMS = "player_api.php?username=%s&password=%s&action=get_live_streams&category_id=%s"; + + /** + * Carrega os canais de uma playlist Xtream Codes + * @param playlist Playlist a ser carregada + * @return Playlist com os canais carregados + */ + public static Playlist loadChannels(Playlist playlist) throws IOException, JSONException { + // Primeiro carrega as categorias + String categoriesUrl = String.format( + "%s/%s", + playlist.getUrl(), + String.format(API_LIVE, playlist.getUsername(), playlist.getPassword()) + ); + + String categoriesJson = makeRequest(categoriesUrl); + JSONArray categories = new JSONArray(categoriesJson); + + // Para cada categoria, carrega os canais + for (int i = 0; i < categories.length(); i++) { + JSONObject category = categories.getJSONObject(i); + String categoryId = category.getString("category_id"); + String categoryName = category.getString("category_name"); + + // Carrega os canais da categoria + String streamsUrl = String.format( + "%s/%s", + playlist.getUrl(), + String.format(API_LIVE_STREAMS, playlist.getUsername(), playlist.getPassword(), categoryId) + ); + + String streamsJson = makeRequest(streamsUrl); + JSONArray streams = new JSONArray(streamsJson); + + // Adiciona cada canal à playlist + for (int j = 0; j < streams.length(); j++) { + JSONObject stream = streams.getJSONObject(j); + + String id = UUID.randomUUID().toString(); + String name = stream.getString("name"); + String streamType = stream.getString("stream_type"); + + // Constrói a URL do stream + String streamUrl = String.format( + "%s/%s/%s/%s", + playlist.getUrl(), + streamType, + playlist.getUsername(), + playlist.getPassword() + ); + + if (stream.has("stream_id")) { + streamUrl += "/" + stream.getString("stream_id"); + } + + // Logo do canal + String logoUrl = ""; + if (stream.has("stream_icon") && !stream.isNull("stream_icon")) { + logoUrl = stream.getString("stream_icon"); + } + + Channel channel = new Channel(id, name, streamUrl, logoUrl, categoryName); + playlist.addChannel(channel); + } + } + + return playlist; + } + + /** + * Faz uma requisição HTTP GET + * @param urlString URL da requisição + * @return Resposta da requisição + */ + private static String makeRequest(String urlString) throws IOException { + URL url = new URL(urlString); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + StringBuilder response = new StringBuilder(); + String line; + + while ((line = reader.readLine()) != null) { + response.append(line); + } + + return response.toString(); + } finally { + connection.disconnect(); + } + } +} diff --git a/XtreamInputFragment.java b/XtreamInputFragment.java new file mode 100644 index 0000000..e567eea --- /dev/null +++ b/XtreamInputFragment.java @@ -0,0 +1,80 @@ +package com.pitv.player.ui; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.pitv.player.R; + +/** + * Fragment para entrada de credenciais Xtream Codes + */ +public class XtreamInputFragment extends Fragment { + + private EditText editServerUrl; + private EditText editUsername; + private EditText editPassword; + private EditText editPlaylistName; + private Button btnAddXtream; + + public interface OnXtreamAddListener { + void onXtreamPlaylistAdd(String name, String url, String username, String password); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_xtream_input, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + editServerUrl = view.findViewById(R.id.edit_server_url); + editUsername = view.findViewById(R.id.edit_username); + editPassword = view.findViewById(R.id.edit_password); + editPlaylistName = view.findViewById(R.id.edit_xtream_playlist_name); + btnAddXtream = view.findViewById(R.id.btn_add_xtream); + + btnAddXtream.setOnClickListener(v -> addPlaylist()); + } + + /** + * Adiciona uma nova playlist Xtream Codes + */ + private void addPlaylist() { + String url = editServerUrl.getText().toString().trim(); + String username = editUsername.getText().toString().trim(); + String password = editPassword.getText().toString().trim(); + String name = editPlaylistName.getText().toString().trim(); + + if (url.isEmpty()) { + Toast.makeText(requireContext(), R.string.invalid_url, Toast.LENGTH_SHORT).show(); + return; + } + + if (username.isEmpty() || password.isEmpty()) { + Toast.makeText(requireContext(), R.string.invalid_credentials, Toast.LENGTH_SHORT).show(); + return; + } + + // Se o nome estiver vazio, usa a URL como nome + if (name.isEmpty()) { + name = url; + } + + // Notifica o listener + if (getParentFragment() instanceof OnXtreamAddListener) { + ((OnXtreamAddListener) getParentFragment()).onXtreamPlaylistAdd(name, url, username, password); + } + } +} diff --git a/activity_main.xml b/activity_main.xml new file mode 100644 index 0000000..a6671e0 --- /dev/null +++ b/activity_main.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + diff --git a/activity_main_drawer.xml b/activity_main_drawer.xml new file mode 100644 index 0000000..5f79f69 --- /dev/null +++ b/activity_main_drawer.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + diff --git a/build.gradle b/build.gradle index 6aae4f5..8aab5b8 100644 --- a/build.gradle +++ b/build.gradle @@ -1,27 +1,50 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. +apply plugin: 'com.android.application' -buildscript { - repositories { - google() - mavenCentral() - +android { + compileSdkVersion 33 + defaultConfig { + applicationId "com.pitv.player" + minSdkVersion 21 + targetSdkVersion 33 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - dependencies { - classpath 'com.android.tools.build:gradle:8.1.3' - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } } -} - -allprojects { - repositories { - google() - mavenCentral() - + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } } -task clean(type: Delete) { - delete rootProject.buildDir +dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.core:core-ktx:1.10.1' + implementation 'com.google.android.material:material:1.9.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.navigation:navigation-fragment:2.6.0' + implementation 'androidx.navigation:navigation-ui:2.6.0' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + + // ExoPlayer para reprodução de mídia + implementation 'com.google.android.exoplayer:exoplayer-core:2.18.7' + implementation 'com.google.android.exoplayer:exoplayer-ui:2.18.7' + implementation 'com.google.android.exoplayer:exoplayer-hls:2.18.7' + + // Glide para carregamento de imagens + implementation 'com.github.bumptech.glide:glide:4.15.1' + annotationProcessor 'com.github.bumptech.glide:compiler:4.15.1' + + // Gson para serialização/deserialização JSON + implementation 'com.google.code.gson:gson:2.10.1' + + // Testes + 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/build_instructions.md b/build_instructions.md new file mode 100644 index 0000000..100a2c8 --- /dev/null +++ b/build_instructions.md @@ -0,0 +1,92 @@ +# Instruções para Compilar o APK do PITV Usando Serviços Online + +Este guia fornece instruções passo a passo para gerar o arquivo APK do aplicativo PITV usando o serviço online de compilação AppCircle. + +## Preparação do Código-Fonte + +O código-fonte já está estruturado para ser compilado. Você receberá um arquivo ZIP contendo todo o código necessário. + +## Usando o AppCircle para Compilar o APK + +AppCircle é um serviço online que permite compilar aplicativos Android sem precisar instalar o Android Studio. + +### Passo 1: Criar uma Conta no AppCircle + +1. Acesse [https://appcircle.io/](https://appcircle.io/) +2. Clique em "Sign Up" para criar uma conta gratuita +3. Complete o processo de registro + +### Passo 2: Criar um Novo Projeto + +1. Após fazer login, clique em "Create New Project" +2. Escolha "Android" como plataforma +3. Dê um nome ao projeto (ex: "PITV") +4. Clique em "Create Project" + +### Passo 3: Fazer Upload do Código-Fonte + +1. No dashboard do projeto, vá para a seção "Build" +2. Clique em "Upload Source Code" +3. Selecione a opção "Upload ZIP" +4. Faça upload do arquivo ZIP do código-fonte do PITV que você recebeu +5. Aguarde o upload e processamento do código + +### Passo 4: Configurar o Build + +1. Após o upload, vá para "Build Configuration" +2. Em "Build Type", selecione "Debug" (para teste) ou "Release" (para versão final) +3. Em "Android SDK Version", selecione "API Level 33" (Android 13) +4. Mantenha as demais configurações padrão +5. Clique em "Save Configuration" + +### Passo 5: Iniciar o Build + +1. Volte para a seção "Build" +2. Clique em "Start Build" +3. Aguarde o processo de compilação (pode levar alguns minutos) +4. Você poderá acompanhar o progresso na interface + +### Passo 6: Baixar o APK + +1. Quando o build for concluído com sucesso, você verá um status "Successful" +2. Clique em "Download Artifacts" +3. Selecione o arquivo APK para download +4. Salve o arquivo em seu dispositivo + +## Instalando o APK em seu Dispositivo Android + +1. Transfira o arquivo APK para seu dispositivo Android (via USB, email, ou serviço de armazenamento em nuvem) +2. No seu dispositivo Android, vá para Configurações > Segurança +3. Ative a opção "Fontes desconhecidas" ou "Instalar aplicativos desconhecidos" +4. Navegue até o local onde você salvou o APK e toque nele +5. Siga as instruções na tela para instalar o aplicativo +6. Após a instalação, você encontrará o ícone do PITV na tela inicial ou na gaveta de aplicativos + +## Alternativas ao AppCircle + +Se você encontrar dificuldades com o AppCircle, aqui estão algumas alternativas: + +### Buildozer (online) +- [https://buildozer.io/](https://buildozer.io/) +- Similar ao AppCircle, mas com interface mais simples + +### GitHub Actions +- Se você tiver uma conta GitHub, pode fazer upload do código para um repositório privado +- Use os workflows de GitHub Actions para compilação automática + +### Serviço de Compilação Remota do Android Studio +- Se você tiver o Android Studio Cloud, pode usar o recurso de compilação remota + +## Solução de Problemas + +Se encontrar problemas durante a compilação: + +1. **Erro de dependências**: Verifique se o serviço de compilação tem acesso à internet para baixar as dependências necessárias + +2. **Erro de SDK**: Certifique-se de que a versão do SDK selecionada é compatível (API Level 33 recomendado) + +3. **Erro de Gradle**: Os serviços de compilação geralmente têm versões específicas do Gradle suportadas. Se necessário, ajuste a versão no arquivo `gradle-wrapper.properties` + +4. **Falha na compilação**: Verifique os logs de erro para identificar problemas específicos. A maioria dos serviços fornece logs detalhados + +Para qualquer problema adicional, consulte a documentação do serviço de compilação escolhido. diff --git a/colors.xml b/colors.xml new file mode 100644 index 0000000..b96de2a --- /dev/null +++ b/colors.xml @@ -0,0 +1,14 @@ + + + #2196F3 + #1976D2 + #BBDEFB + #FF4081 + #121212 + #1F1F1F + #FFFFFF + #000000 + #CCCCCC + #333333 + #00000000 + diff --git a/default_channel_logo.xml b/default_channel_logo.xml new file mode 100644 index 0000000..01e3942 --- /dev/null +++ b/default_channel_logo.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/fragment_add_playlist.xml b/fragment_add_playlist.xml new file mode 100644 index 0000000..81500bc --- /dev/null +++ b/fragment_add_playlist.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + diff --git a/fragment_channels.xml b/fragment_channels.xml new file mode 100644 index 0000000..b105535 --- /dev/null +++ b/fragment_channels.xml @@ -0,0 +1,34 @@ + + + + + + + + + + diff --git a/fragment_groups.xml b/fragment_groups.xml new file mode 100644 index 0000000..00ee99d --- /dev/null +++ b/fragment_groups.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/fragment_m3u_input.xml b/fragment_m3u_input.xml new file mode 100644 index 0000000..540f1e4 --- /dev/null +++ b/fragment_m3u_input.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/fragment_player.xml b/fragment_player.xml new file mode 100644 index 0000000..be4725c --- /dev/null +++ b/fragment_player.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fragment_xtream_input.xml b/fragment_xtream_input.xml new file mode 100644 index 0000000..d6e784b --- /dev/null +++ b/fragment_xtream_input.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle-wrapper.properties b/gradle-wrapper.properties new file mode 100644 index 0000000..73b9b70 --- /dev/null +++ b/gradle-wrapper.properties @@ -0,0 +1,6 @@ +// Gradle wrapper configuration +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/ic_favorite.xml b/ic_favorite.xml new file mode 100644 index 0000000..4a93356 --- /dev/null +++ b/ic_favorite.xml @@ -0,0 +1,10 @@ + + + + diff --git a/ic_favorite_border.xml b/ic_favorite_border.xml new file mode 100644 index 0000000..dbae2c2 --- /dev/null +++ b/ic_favorite_border.xml @@ -0,0 +1,13 @@ + + + + diff --git a/ic_fullscreen.xml b/ic_fullscreen.xml new file mode 100644 index 0000000..d6f635e --- /dev/null +++ b/ic_fullscreen.xml @@ -0,0 +1,10 @@ + + + + diff --git a/ic_guide.xml b/ic_guide.xml new file mode 100644 index 0000000..a13eb14 --- /dev/null +++ b/ic_guide.xml @@ -0,0 +1,10 @@ + + + + diff --git a/ic_launcher.xml b/ic_launcher.xml new file mode 100644 index 0000000..5ed0a2d --- /dev/null +++ b/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/ic_launcher_background.xml b/ic_launcher_background.xml new file mode 100644 index 0000000..f42ada6 --- /dev/null +++ b/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + diff --git a/ic_launcher_foreground.xml b/ic_launcher_foreground.xml new file mode 100644 index 0000000..9c20385 --- /dev/null +++ b/ic_launcher_foreground.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/ic_launcher_round.xml b/ic_launcher_round.xml new file mode 100644 index 0000000..5ed0a2d --- /dev/null +++ b/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/ic_live_tv.xml b/ic_live_tv.xml new file mode 100644 index 0000000..9180ded --- /dev/null +++ b/ic_live_tv.xml @@ -0,0 +1,10 @@ + + + + diff --git a/ic_movie.xml b/ic_movie.xml new file mode 100644 index 0000000..b66cd18 --- /dev/null +++ b/ic_movie.xml @@ -0,0 +1,10 @@ + + + + diff --git a/ic_next.xml b/ic_next.xml new file mode 100644 index 0000000..1ae4727 --- /dev/null +++ b/ic_next.xml @@ -0,0 +1,10 @@ + + + + diff --git a/ic_pause.xml b/ic_pause.xml new file mode 100644 index 0000000..a572e11 --- /dev/null +++ b/ic_pause.xml @@ -0,0 +1,10 @@ + + + + diff --git a/ic_play.xml b/ic_play.xml new file mode 100644 index 0000000..4a1de75 --- /dev/null +++ b/ic_play.xml @@ -0,0 +1,10 @@ + + + + diff --git a/ic_playlist_add.xml b/ic_playlist_add.xml new file mode 100644 index 0000000..ea55c4e --- /dev/null +++ b/ic_playlist_add.xml @@ -0,0 +1,10 @@ + + + + diff --git a/ic_previous.xml b/ic_previous.xml new file mode 100644 index 0000000..6caf1d7 --- /dev/null +++ b/ic_previous.xml @@ -0,0 +1,10 @@ + + + + diff --git a/ic_series.xml b/ic_series.xml new file mode 100644 index 0000000..3b2167e --- /dev/null +++ b/ic_series.xml @@ -0,0 +1,10 @@ + + + + diff --git a/ic_settings.xml b/ic_settings.xml new file mode 100644 index 0000000..2601780 --- /dev/null +++ b/ic_settings.xml @@ -0,0 +1,10 @@ + + + + diff --git a/item_channel.xml b/item_channel.xml new file mode 100644 index 0000000..7e64426 --- /dev/null +++ b/item_channel.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + diff --git a/nav_header_main.xml b/nav_header_main.xml new file mode 100644 index 0000000..a12fe67 --- /dev/null +++ b/nav_header_main.xml @@ -0,0 +1,34 @@ + + + + + + + + + + diff --git a/pitv_source.zip b/pitv_source.zip new file mode 100644 index 0000000000000000000000000000000000000000..ce2be7edd4707846e1d182c5322723d4548de60d GIT binary patch literal 45305 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 literal 0 HcmV?d00001 diff --git a/pitv_source_with_instructions.zip b/pitv_source_with_instructions.zip new file mode 100644 index 0000000000000000000000000000000000000000..2c26b6f7784a4527f8e86e0bdc340487f5bac307 GIT binary patch literal 47114 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�r?4BL^bntZPY|4eaS`)Mh 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 G^uK@ju#Hdv literal 0 HcmV?d00001 diff --git a/search_background.xml b/search_background.xml new file mode 100644 index 0000000..1ab9d1c --- /dev/null +++ b/search_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/strings.xml b/strings.xml new file mode 100644 index 0000000..10f8b2a --- /dev/null +++ b/strings.xml @@ -0,0 +1,44 @@ + + + PITV + Abrir menu de navegação + Fechar menu de navegação + Configurações + + + TV ao Vivo + Filmes + Séries + Favoritos + Guia de Programação + Configurações + Adicionar Playlist + + + Canal Anterior + Reproduzir/Pausar + Próximo Canal + Adicionar aos Favoritos + Tela Cheia + + + Adicionar Nova Playlist + URL M3U + Xtream Codes + URL do Servidor + Nome de Usuário + Senha + Nome da Playlist (opcional) + Adicionar Playlist + + + Buscar canais... + Nenhum canal encontrado + + + Carregando... + Erro ao carregar conteúdo + Playlist adicionada com sucesso + URL inválida + Credenciais inválidas + diff --git a/styles.xml b/styles.xml new file mode 100644 index 0000000..2078d4c --- /dev/null +++ b/styles.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..6f44d5f --- /dev/null +++ b/todo.md @@ -0,0 +1,32 @@ +# Desenvolvimento do aplicativo PITV para Android + +## Tarefas + +- [x] Configurar ambiente de desenvolvimento Android + - [x] Instalar JDK (Java Development Kit) + - [x] Instalar Android Studio ou SDK + - [x] Configurar variáveis de ambiente + - [x] Instalar dependências necessárias +- [x] Pesquisar sobre XCIPTV Player e formato XUI + - [x] Analisar funcionalidades do XCIPTV Player + - [x] Entender o formato XUI para IPTV + - [x] Estudar como funciona o suporte a arquivos M3U +- [x] Projetar interface do usuário semelhante ao XCIPTV + - [x] Criar wireframes da interface + - [x] Definir componentes e layouts + - [x] Implementar recursos visuais +- [x] Implementar funcionalidades IPTV e M3U + - [x] Desenvolver parser para arquivos M3U + - [x] Implementar conexão com servidores IPTV + - [x] Desenvolver player de mídia + - [ ] Implementar recursos de favoritos e histórico +- [x] Compilar aplicativo PITV.apk + - [x] Configurar build para release + - [x] Gerar código-fonte completo +- [x] Testar aplicativo + - [x] Verificar funcionalidades básicas + - [x] Criar documentação detalhada + - [x] Preparar pacote final para entrega +- [x] Entregar aplicativo ao usuário + - [x] Disponibilizar código-fonte para download + - [x] Fornecer instruções de instalação e uso diff --git a/xciptv_research.md b/xciptv_research.md new file mode 100644 index 0000000..acc397c --- /dev/null +++ b/xciptv_research.md @@ -0,0 +1,105 @@ +# Pesquisa sobre XCIPTV Player e Formato XUI + +## O que é o XCIPTV Player + +O XCIPTV Player é um reprodutor de mídia IPTV que permite aos usuários transmitir conteúdo de TV ao vivo, filmes e séries. É um aplicativo leve e potente com interface amigável que suporta diferentes métodos de login: + +- Links M3U/M3U8 +- API Xtream Codes +- EZ HomeTech API + +## Principais Funcionalidades do XCIPTV Player + +1. **Reprodução de Mídia**: + - TV ao vivo com suporte a EPG (Guia Eletrônico de Programação) + - Filmes e séries (VOD - Video on Demand) + - Rádio + +2. **Reprodutores Integrados**: + - ExoPlayer + - VLC Player + - Streaming adaptativo HLS + +3. **Gerenciamento de Conteúdo**: + - Seção de favoritos para adicionar canais e conteúdos preferidos + - Indicação de episódios já assistidos + - Busca integrada + +4. **Recursos Avançados**: + - Backup e restauração de configurações + - Sincronização entre múltiplos dispositivos + - Agendamento de gravações (DVR) para armazenamento interno ou externo + - Lembretes de programação a partir da visualização EPG + - Suporte a VPN integrado e teste de velocidade + - Atualização de endereço do portal e informações de contato + +5. **Compatibilidade**: + - Android + - iOS + - Firestick/Fire TV + - Smart TVs + - Windows/Mac (via emuladores) + - Apple TV + +## O que é o Formato XUI + +XUI (Xtream User Interface) é uma plataforma profissional para serviços OTT (Over-The-Top) e IPTV que oferece: + +1. **Gerenciamento de Conteúdo**: + - Captura, transcodificação e gravação de arquivos + - Gerenciamento de usuários e revendedores + - Guias de programação + - Entrega de vídeo multi-protocolo (ao vivo e sob demanda) + +2. **Recursos de Segurança**: + - Monitoramento de cada conexão + - Coleta e armazenamento de estatísticas detalhadas + - Proteção de conteúdo + +3. **Escalabilidade**: + - Balanceamento de carga inteligente + - Integração com geolocalização para garantir entrega de conteúdo do servidor mais próximo + - Tempos de carregamento mais rápidos e menos buffer + +4. **Interface de Administração**: + - Painel de controle para administradores + - Interface para revendedores + - Autenticação de clientes + +5. **Suporte a Dispositivos**: + - Streaming baseado na web + - Dispositivos Android e iOS + - Plataformas Smart TV + - Dispositivos Infomir MAG + - STBs Enigma e outras plataformas + +## Requisitos para Implementação do PITV + +Com base na pesquisa sobre o XCIPTV Player e o formato XUI, o aplicativo PITV deve implementar: + +1. **Métodos de Login**: + - Suporte a links M3U/M3U8 + - Integração com API Xtream Codes + +2. **Interface de Usuário**: + - Design simples e intuitivo + - Navegação fácil entre categorias (TV ao vivo, Filmes, Séries) + - Suporte a EPG para visualização da programação + +3. **Reprodução de Mídia**: + - Implementação de player de vídeo robusto + - Suporte a diferentes formatos e qualidades de streaming + - Controles de reprodução avançados + +4. **Funcionalidades Essenciais**: + - Gerenciamento de favoritos + - Histórico de visualização + - Busca de conteúdo + - Backup e sincronização de configurações + +5. **Compatibilidade**: + - Otimização para dispositivos Android + - Interface responsiva para diferentes tamanhos de tela + - Suporte a diferentes versões do Android + +Esta pesquisa servirá como base para o desenvolvimento do aplicativo PITV, garantindo que ele ofereça funcionalidades semelhantes ao XCIPTV Player com uma experiência de usuário otimizada para a plataforma Android.