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 0000000..ce2be7e Binary files /dev/null and b/pitv_source.zip differ diff --git a/pitv_source_with_instructions.zip b/pitv_source_with_instructions.zip new file mode 100644 index 0000000..2c26b6f Binary files /dev/null and b/pitv_source_with_instructions.zip differ 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.