diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0e7c882e..7a814b1e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.TPStreamsAndroidPlayer" + android:enableOnBackInvokedCallback="true" tools:targetApi="31"> + + + + \ No newline at end of file diff --git a/app/src/main/java/com/tpstreams/player/DownloadsActivity.kt b/app/src/main/java/com/tpstreams/player/DownloadsActivity.kt new file mode 100644 index 00000000..110f619b --- /dev/null +++ b/app/src/main/java/com/tpstreams/player/DownloadsActivity.kt @@ -0,0 +1,548 @@ +package com.tpstreams.player + +import android.content.Context +import android.content.Intent +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.ImageButton +import android.widget.ProgressBar +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.offline.Download +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.button.MaterialButton +import com.tpstreams.player.databinding.ActivityDownloadsBinding +import com.tpstreams.player.offline.DownloadUtils +import com.tpstreams.player.utils.NetworkUtils + +@UnstableApi +class DownloadsActivity : AppCompatActivity() { + + private lateinit var binding: ActivityDownloadsBinding + private lateinit var adapter: DownloadAdapter + private val TAG = "DownloadsActivity" + + // Map to track currently playing content + private val playersMap = mutableMapOf() + + // Handler for updating download progress + private val handler = Handler(Looper.getMainLooper()) + private val updateRunnable = object : Runnable { + override fun run() { + updateDownloadProgress() + handler.postDelayed(this, 1000) // Update every second + } + } + + // Add this property to the DownloadsActivity class + private val pausedDownloads = mutableMapOf() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityDownloadsBinding.inflate(layoutInflater) + setContentView(binding.root) + + // Set up toolbar + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.title = "Downloads" + + // Set up RecyclerView + binding.recyclerView.layoutManager = LinearLayoutManager(this) + binding.recyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) + + // Create adapter + adapter = DownloadAdapter() + binding.recyclerView.adapter = adapter + + // Load downloads + loadDownloads() + + // Check network status + if (NetworkUtils.isOfflineMode(this)) { + Toast.makeText(this, "Offline mode - Playing downloaded videos only", Toast.LENGTH_SHORT).show() + } + + // Restore saved state if available + savedInstanceState?.let { restoreSavedState(it) } + + // Restore paused downloads from database + restorePausedDownloadsState() + + // Start progress updates + handler.post(updateRunnable) + } + + override fun onResume() { + super.onResume() + loadDownloads() + handler.post(updateRunnable) + } + + override fun onPause() { + super.onPause() + handler.removeCallbacks(updateRunnable) + } + + override fun onDestroy() { + super.onDestroy() + // Remove callbacks + handler.removeCallbacks(updateRunnable) + + // Release all players + playersMap.values.forEach { it.release() } + playersMap.clear() + } + + override fun onSupportNavigateUp(): Boolean { + finish() + return true + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + + // Save paused downloads state + val pausedArray = pausedDownloads.keys.toTypedArray() + outState.putStringArray("paused_downloads", pausedArray) + } + + private fun restoreSavedState(savedInstanceState: Bundle) { + // Restore paused downloads + val pausedArray = savedInstanceState.getStringArray("paused_downloads") + pausedArray?.forEach { contentId -> + pausedDownloads[contentId] = true + } + } + + /** + * Restore paused downloads state from the database + */ + private fun restorePausedDownloadsState() { + try { + // Get all downloads from the database + val downloads = DownloadUtils.getDownloads(this) + + // Check each download to see if it's paused + downloads.forEach { download -> + if (download.state == Download.STATE_STOPPED && download.stopReason == 1) { + // This download is paused, add it to the pausedDownloads map + pausedDownloads[download.request.id] = true + Log.d(TAG, "Restored paused state for download: ${download.request.id}") + } + } + } catch (e: Exception) { + Log.e(TAG, "Error restoring paused downloads state: ${e.message}", e) + } + } + + private fun loadDownloads() { + // Use DownloadUtils to get simplified download items + val downloadItems = DownloadUtils.getDownloadItems(this) + + if (downloadItems.isEmpty()) { + binding.recyclerView.visibility = View.GONE + binding.emptyView.visibility = View.VISIBLE + } else { + binding.recyclerView.visibility = View.VISIBLE + binding.emptyView.visibility = View.GONE + adapter.updateDownloads(downloadItems) + } + } + + private fun updateDownloadProgress() { + try { + val downloadItems = DownloadUtils.getDownloadItems(this) + if (downloadItems.isNotEmpty()) { + adapter.updateDownloadProgress(downloadItems) + } + } catch (e: Exception) { + Log.e(TAG, "Error updating download progress: ${e.message}", e) + } + } + + private fun showDeleteConfirmation(downloadItem: DownloadUtils.DownloadItem) { + AlertDialog.Builder(this) + .setTitle("Delete Download") + .setMessage("Are you sure you want to delete this download?") + .setPositiveButton("Delete") { _, _ -> + deleteDownload(downloadItem) + } + .setNegativeButton("Cancel", null) + .show() + } + + private fun deleteDownload(downloadItem: DownloadUtils.DownloadItem) { + // Show deletion progress dialog + val progressDialog = AlertDialog.Builder(this) + .setTitle("Deleting") + .setMessage("Deleting download...") + .setCancelable(false) + .create() + + progressDialog.show() + + // Stop playback if it's playing + stopPlayback(downloadItem.contentId) + + // Use a background thread for deletion to avoid blocking the UI + Thread { + try { + // Delete the download + DownloadUtils.deleteDownload(this, downloadItem.contentId) + + // Run on UI thread to update the UI + runOnUiThread { + progressDialog.dismiss() + + // Remove the item from the adapter + adapter.removeDownload(downloadItem.contentId) + + // Check if the list is now empty + if (adapter.itemCount == 0) { + binding.recyclerView.visibility = View.GONE + binding.emptyView.visibility = View.VISIBLE + } + + Toast.makeText(this, "Download deleted successfully", Toast.LENGTH_SHORT).show() + } + } catch (e: Exception) { + // Handle any errors + runOnUiThread { + progressDialog.dismiss() + Toast.makeText(this, "Error deleting download: ${e.message}", Toast.LENGTH_SHORT).show() + Log.e(TAG, "Error deleting download: ${e.message}", e) + } + } + }.start() + } + + private fun playDownload(contentId: String, viewHolder: DownloadAdapter.ViewHolder) { + try { + // First verify the download is complete and valid + if (!DownloadUtils.verifyDownload(this, contentId)) { + Toast.makeText(this, "Content not fully downloaded or download is invalid", Toast.LENGTH_SHORT).show() + return + } + + // Launch PlayerActivity with the downloaded content + val intent = Intent(this, PlayerActivity::class.java).apply { + putExtra(PlayerActivity.EXTRA_CONTENT_TYPE, PlayerActivity.CONTENT_TYPE_DOWNLOAD) + putExtra(PlayerActivity.EXTRA_CONTENT_ID, contentId) + } + startActivity(intent) + + } catch (e: Exception) { + Log.e(TAG, "Error playing download: ${e.message}", e) + Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_SHORT).show() + } + } + + /** + * Pause a download + * @param contentId The ID of the content to pause + */ + private fun pauseDownload(contentId: String) { + try { + Log.d(TAG, "Attempting to pause download for contentId: $contentId") + + // Call the actual pause download method from DownloadUtils + DownloadUtils.pauseDownload(this, contentId) + + Toast.makeText(this, "Download paused", Toast.LENGTH_SHORT).show() + + // Store the paused state in a map + pausedDownloads[contentId] = true + Log.d(TAG, "Added contentId to pausedDownloads map: $contentId") + + // Force refresh the UI to show the paused state + refreshDownloads() + + // Verify the download was actually paused + val isPaused = DownloadUtils.isDownloadPaused(this, contentId) + Log.d(TAG, "After pause operation, isDownloadPaused = $isPaused for contentId: $contentId") + } catch (e: Exception) { + Log.e(TAG, "Error pausing download: ${e.message}", e) + Toast.makeText(this, "Error pausing download", Toast.LENGTH_SHORT).show() + } + } + + /** + * Resume a paused download + * @param contentId The ID of the content to resume + */ + private fun resumeDownload(contentId: String) { + try { + Log.d(TAG, "Attempting to resume download for contentId: $contentId") + + // Call the actual resume download method from DownloadUtils + DownloadUtils.resumeDownload(this, contentId) + + // Remove the paused state from the map + pausedDownloads.remove(contentId) + Log.d(TAG, "Removed contentId from pausedDownloads map: $contentId") + + // Force refresh the UI to show the resumed state + refreshDownloads() + + // Verify the download was actually resumed + val isPaused = DownloadUtils.isDownloadPaused(this, contentId) + Log.d(TAG, "After resume operation, isDownloadPaused = $isPaused for contentId: $contentId") + + // If the download is still paused, try restarting it + if (isPaused) { + Log.d(TAG, "Resume failed, attempting to restart download for contentId: $contentId") + + // Show a progress dialog while restarting + val progressDialog = AlertDialog.Builder(this) + .setTitle("Restarting Download") + .setMessage("Attempting to restart download...") + .setCancelable(false) + .create() + + progressDialog.show() + + // Use a background thread for restarting + Thread { + try { + // Restart the download + DownloadUtils.restartDownload(this, contentId) + + // Run on UI thread to update the UI + runOnUiThread { + progressDialog.dismiss() + Toast.makeText(this, "Download restarted", Toast.LENGTH_SHORT).show() + refreshDownloads() + } + } catch (e: Exception) { + // Handle any errors + runOnUiThread { + progressDialog.dismiss() + Toast.makeText(this, "Error restarting download: ${e.message}", Toast.LENGTH_SHORT).show() + Log.e(TAG, "Error restarting download: ${e.message}", e) + } + } + }.start() + } else { + Toast.makeText(this, "Download resumed", Toast.LENGTH_SHORT).show() + } + } catch (e: Exception) { + Log.e(TAG, "Error resuming download: ${e.message}", e) + Toast.makeText(this, "Error resuming download", Toast.LENGTH_SHORT).show() + } + } + + /** + * Refresh the downloads list + */ + private fun refreshDownloads() { + val downloads = DownloadUtils.getDownloadItems(this) + adapter.updateDownloadProgress(downloads) + } + + private fun stopPlayback(contentId: String) { + try { + val player = playersMap.remove(contentId) ?: return + player.stop() + player.release() + } catch (e: Exception) { + Log.e(TAG, "Error stopping playback: ${e.message}", e) + } + } + + /** + * RecyclerView adapter for displaying downloads + */ + inner class DownloadAdapter : RecyclerView.Adapter() { + + private var downloads: MutableList = mutableListOf() + private val viewHolders = mutableMapOf() + + fun updateDownloads(newDownloads: List) { + downloads.clear() + downloads.addAll(newDownloads) + notifyDataSetChanged() + } + + fun removeDownload(contentId: String) { + val position = downloads.indexOfFirst { it.contentId == contentId } + if (position != -1) { + downloads.removeAt(position) + notifyItemRemoved(position) + viewHolders.remove(contentId) + } + } + + fun updateDownloadProgress(updatedDownloads: List) { + try { + // Update progress for existing items without full refresh + updatedDownloads.forEach { updatedItem -> + val existingItem = downloads.find { it.contentId == updatedItem.contentId } + if (existingItem != null && + (existingItem.progress != updatedItem.progress || + existingItem.status != updatedItem.status || + existingItem.isComplete != updatedItem.isComplete)) { + + // Update the ViewHolder if it's visible + viewHolders[updatedItem.contentId]?.updateProgress(updatedItem) + } + } + + // If the list has changed (items added/removed), do a full refresh + if (downloads.map { it.contentId }.toSet() != updatedDownloads.map { it.contentId }.toSet()) { + downloads.clear() + downloads.addAll(updatedDownloads) + notifyDataSetChanged() + } + } catch (e: Exception) { + Log.e(TAG, "Error updating download progress in adapter: ${e.message}", e) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_download, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val downloadItem = downloads[position] + holder.bind(downloadItem) + viewHolders[downloadItem.contentId] = holder + } + + override fun getItemCount(): Int = downloads.size + + inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val titleTextView: TextView = itemView.findViewById(R.id.titleTextView) + private val statusTextView: TextView = itemView.findViewById(R.id.statusTextView) + private val progressBar: ProgressBar = itemView.findViewById(R.id.progressBar) + private val playButton: MaterialButton = itemView.findViewById(R.id.playButton) + private val pauseButton: MaterialButton = itemView.findViewById(R.id.pauseButton) + private val resumeButton: MaterialButton = itemView.findViewById(R.id.resumeButton) + private val deleteButton: MaterialButton = itemView.findViewById(R.id.deleteButton) + + private var currentContentId: String? = null + private var currentItem: DownloadUtils.DownloadItem? = null + + fun bind(downloadItem: DownloadUtils.DownloadItem) { + currentContentId = downloadItem.contentId + currentItem = downloadItem + + titleTextView.text = downloadItem.contentId + + // Update UI based on download status + updateProgress(downloadItem) + + // Set up click listeners + playButton.setOnClickListener { + currentContentId?.let { contentId -> + playDownload(contentId, this) + } + } + + pauseButton.setOnClickListener { + currentContentId?.let { contentId -> + pauseDownload(contentId) + pauseButton.visibility = View.GONE + resumeButton.visibility = View.VISIBLE + } + } + + resumeButton.setOnClickListener { + currentContentId?.let { contentId -> + resumeDownload(contentId) + resumeButton.visibility = View.GONE + pauseButton.visibility = View.VISIBLE + } + } + + deleteButton.setOnClickListener { + showDeleteConfirmation(downloadItem) + } + } + + fun updateProgress(downloadItem: DownloadUtils.DownloadItem) { + currentItem = downloadItem + + // Show appropriate UI based on download status + if (downloadItem.isComplete) { + // Download is complete - show play button, hide progress and pause/resume buttons + progressBar.visibility = View.GONE + statusTextView.text = "Downloaded" + + // Show play button, hide pause/resume buttons + playButton.visibility = View.VISIBLE + pauseButton.visibility = View.GONE + resumeButton.visibility = View.GONE + + Log.d(TAG, "Download complete for contentId: ${downloadItem.contentId}, showing play button") + } else { + // Download is in progress - show progress and pause/resume button + progressBar.visibility = View.VISIBLE + progressBar.progress = downloadItem.progress + statusTextView.text = "Downloading ${downloadItem.progress}%" + + // Check if download is actually paused using the library function + val isPaused = DownloadUtils.isDownloadPaused(itemView.context, downloadItem.contentId) + Log.d(TAG, "Download in progress for contentId: ${downloadItem.contentId}, progress: ${downloadItem.progress}%, isPaused: $isPaused") + + // Show play button if complete, otherwise hide it + playButton.visibility = View.GONE + + // Show pause button if not paused, otherwise show resume button + if (isPaused) { + pauseButton.visibility = View.GONE + resumeButton.visibility = View.VISIBLE + statusTextView.text = "Paused ${downloadItem.progress}%" + Log.d(TAG, "Showing resume button for contentId: ${downloadItem.contentId}") + } else { + pauseButton.visibility = View.VISIBLE + resumeButton.visibility = View.GONE + Log.d(TAG, "Showing pause button for contentId: ${downloadItem.contentId}") + } + } + } + + fun updatePlaybackControls(isPlaying: Boolean) { + // This method is only relevant for completed downloads being played + if (currentItem?.isComplete == true) { + if (isPlaying) { + playButton.visibility = View.GONE + pauseButton.visibility = View.VISIBLE + resumeButton.visibility = View.GONE + } else { + // Check if we have a paused player for this content + val hasPausedPlayer = currentContentId?.let { contentId -> + val player = playersMap[contentId] + player != null && player.playbackState != Player.STATE_IDLE + } ?: false + + if (hasPausedPlayer) { + playButton.visibility = View.GONE + pauseButton.visibility = View.GONE + resumeButton.visibility = View.VISIBLE + } else { + playButton.visibility = View.VISIBLE + pauseButton.visibility = View.GONE + resumeButton.visibility = View.GONE + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tpstreams/player/MainActivity.kt b/app/src/main/java/com/tpstreams/player/MainActivity.kt index 4fc1ed42..403e1c74 100644 --- a/app/src/main/java/com/tpstreams/player/MainActivity.kt +++ b/app/src/main/java/com/tpstreams/player/MainActivity.kt @@ -1,19 +1,30 @@ package com.tpstreams.player +import android.content.Intent import android.os.Bundle import android.util.Log -import androidx.activity.viewModels +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.OptIn import androidx.appcompat.app.AppCompatActivity import androidx.media3.common.util.UnstableApi import com.tpstreams.player.databinding.ActivityMainBinding +import com.tpstreams.player.utils.NetworkUtils +@UnstableApi class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding - private val viewModel: PlayerUIViewModel by viewModels() + private val TAG = "MainActivity" + + // DRM content credentials + private val drmContentId = "3G2p5NdMaRu" // DRM content ID + private val drmAccessToken = "328f6f1c-c188-4c3f-8e38-345c9aaa1a51" // DRM access token + + // Non-DRM content credentials + private val nonDrmContentId = "ACGhHuD7DEa" // Non-DRM content ID + private val nonDrmAccessToken = "5bea276d-7882-4f8f-951a-c628622817e0" // Non-DRM access token - @OptIn(UnstableApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -21,9 +32,53 @@ class MainActivity : AppCompatActivity() { binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) - // Initialize SDK once - TPStreamsPlayer.init("6332n7") + // Initialize SDK once with application context + TPStreamsPlayer.init("9q94nm", applicationContext) - binding.playerView.player = viewModel.player + // Set up button click listeners + setupButtonListeners() + + // Check network status + if (NetworkUtils.isOfflineMode(this)) { + Toast.makeText(this, "You are in offline mode. Only downloaded videos can be played.", Toast.LENGTH_LONG).show() + } + } + + private fun setupButtonListeners() { + // DRM button click listener + binding.drmButton.setOnClickListener { + if (NetworkUtils.isOfflineMode(this)) { + Toast.makeText(this, "Cannot play DRM content in offline mode", Toast.LENGTH_SHORT).show() + return@setOnClickListener + } + + val intent = Intent(this, PlayerActivity::class.java).apply { + putExtra(PlayerActivity.EXTRA_CONTENT_TYPE, PlayerActivity.CONTENT_TYPE_DRM) + putExtra(PlayerActivity.EXTRA_CONTENT_ID, drmContentId) + putExtra(PlayerActivity.EXTRA_ACCESS_TOKEN, drmAccessToken) + } + startActivity(intent) + } + + // Non-DRM button click listener + binding.nonDrmButton.setOnClickListener { + if (NetworkUtils.isOfflineMode(this)) { + Toast.makeText(this, "Cannot play streaming content in offline mode", Toast.LENGTH_SHORT).show() + return@setOnClickListener + } + + val intent = Intent(this, PlayerActivity::class.java).apply { + putExtra(PlayerActivity.EXTRA_CONTENT_TYPE, PlayerActivity.CONTENT_TYPE_NON_DRM) + putExtra(PlayerActivity.EXTRA_CONTENT_ID, nonDrmContentId) + putExtra(PlayerActivity.EXTRA_ACCESS_TOKEN, nonDrmAccessToken) + } + startActivity(intent) + } + + // Downloads button click listener + binding.downloadsButton.setOnClickListener { + val intent = Intent(this, DownloadsActivity::class.java) + startActivity(intent) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/tpstreams/player/PlayerActivity.kt b/app/src/main/java/com/tpstreams/player/PlayerActivity.kt new file mode 100644 index 00000000..63353a08 --- /dev/null +++ b/app/src/main/java/com/tpstreams/player/PlayerActivity.kt @@ -0,0 +1,159 @@ +package com.tpstreams.player + +import android.os.Bundle +import android.util.Log +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import com.tpstreams.player.databinding.ActivityPlayerBinding +import com.tpstreams.player.offline.DownloadUtils +import com.tpstreams.player.utils.NetworkUtils + +@UnstableApi +class PlayerActivity : AppCompatActivity() { + + private lateinit var binding: ActivityPlayerBinding + private var player: TPStreamsPlayer? = null + private val TAG = "PlayerActivity" + + companion object { + const val EXTRA_CONTENT_TYPE = "content_type" + const val EXTRA_CONTENT_ID = "content_id" + const val EXTRA_ACCESS_TOKEN = "access_token" + + const val CONTENT_TYPE_DRM = "drm" + const val CONTENT_TYPE_NON_DRM = "non_drm" + const val CONTENT_TYPE_DOWNLOAD = "download" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityPlayerBinding.inflate(layoutInflater) + setContentView(binding.root) + + // Initialize SDK + TPStreamsPlayer.init("9q94nm", applicationContext) + + // Get content type and details from intent + val contentType = intent.getStringExtra(EXTRA_CONTENT_TYPE) ?: "" + val contentId = intent.getStringExtra(EXTRA_CONTENT_ID) ?: "" + val accessToken = intent.getStringExtra(EXTRA_ACCESS_TOKEN) ?: "" + + // Set up back button with appropriate text + if (contentType == CONTENT_TYPE_DOWNLOAD) { + binding.backButton.text = "Back to Downloads" + } else { + binding.backButton.text = "Back to Home" + } + + binding.backButton.setOnClickListener { + finish() + } + + // Check network status for streaming content + if ((contentType == CONTENT_TYPE_DRM || contentType == CONTENT_TYPE_NON_DRM) && + NetworkUtils.isOfflineMode(this)) { + Toast.makeText(this, "Cannot play streaming content in offline mode", Toast.LENGTH_LONG).show() + finish() + return + } + + // Initialize player based on content type + when (contentType) { + CONTENT_TYPE_DRM -> { + binding.contentTitle.text = "DRM Protected Content" + initializeStreamingPlayer(contentId, accessToken) + } + CONTENT_TYPE_NON_DRM -> { + binding.contentTitle.text = "Non-DRM Content" + initializeStreamingPlayer(contentId, accessToken) + } + CONTENT_TYPE_DOWNLOAD -> { + binding.contentTitle.text = "Downloaded Content" + initializeOfflinePlayer(contentId) + } + else -> { + Toast.makeText(this, "Invalid content type", Toast.LENGTH_SHORT).show() + finish() + } + } + } + + private fun initializeStreamingPlayer(contentId: String, accessToken: String) { + try { + // Check for internet connectivity + if (!NetworkUtils.isNetworkAvailable(this)) { + Toast.makeText(this, "No internet connection available. Cannot play streaming content.", Toast.LENGTH_LONG).show() + binding.contentTitle.text = "${binding.contentTitle.text} (No Internet)" + return + } + + player = TPStreamsPlayer.create( + context = applicationContext, + assetId = contentId, + accessToken = accessToken, + shouldAutoPlay = true + ) + + setupPlayerView() + } catch (e: Exception) { + Log.e(TAG, "Error initializing streaming player: ${e.message}", e) + Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_SHORT).show() + } + } + + private fun initializeOfflinePlayer(contentId: String) { + try { + // First verify the download is complete and valid + if (!DownloadUtils.verifyDownload(this, contentId)) { + Toast.makeText(this, "Content not fully downloaded or download is invalid", Toast.LENGTH_SHORT).show() + finish() + return + } + + player = TPStreamsPlayer.create( + this, + contentId, + "", // No access token needed for offline playback + false // Don't auto-play, we'll control it manually + ) + + val success = DownloadUtils.playOfflineContent(player!!, contentId) + if (success) { + player?.play() + setupPlayerView() + } else { + Toast.makeText(this, "Failed to play downloaded content", Toast.LENGTH_SHORT).show() + finish() + } + } catch (e: Exception) { + Log.e(TAG, "Error playing offline content: ${e.message}", e) + Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_SHORT).show() + finish() + } + } + + private fun setupPlayerView() { + binding.playerView.player = player + binding.playerView.showController() + + // Add error listener + player?.addListener(object : Player.Listener { + override fun onPlayerError(error: androidx.media3.common.PlaybackException) { + Log.e(TAG, "Player error: ${error.message}") + Toast.makeText( + this@PlayerActivity, + "Playback error: ${error.message}", + Toast.LENGTH_SHORT + ).show() + } + }) + } + + override fun onDestroy() { + super.onDestroy() + player?.release() + player = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tpstreams/player/PlayerUIViewModel.kt b/app/src/main/java/com/tpstreams/player/PlayerUIViewModel.kt index b9b3f6e0..d945da34 100644 --- a/app/src/main/java/com/tpstreams/player/PlayerUIViewModel.kt +++ b/app/src/main/java/com/tpstreams/player/PlayerUIViewModel.kt @@ -9,14 +9,27 @@ const val TAG = "PlayerUIViewModel" class PlayerUIViewModel(application: Application) : AndroidViewModel(application) { // Use application context to avoid memory leaks - val player: TPStreamsPlayer by lazy { - TPStreamsPlayer.create( + var player: TPStreamsPlayer = createDefaultPlayer(application) + + private fun createDefaultPlayer(application: Application): TPStreamsPlayer { + // Initialize the SDK with the correct organization ID + TPStreamsPlayer.init("9q94nm", application.applicationContext) + + return TPStreamsPlayer.create( context = application.applicationContext, - assetId = "8rEx9apZHFF", - accessToken = "19aa0055-d965-4654-8fce-b804e70a46b0", + assetId = "ACGhHuD7DEa", // Non-DRM content ID + accessToken = "5bea276d-7882-4f8f-951a-c628622817e0", shouldAutoPlay = false ) } + + fun updatePlayer(newPlayer: TPStreamsPlayer) { + // Release the old player + player.release() + + // Update with the new player + player = newPlayer + } override fun onCleared() { super.onCleared() diff --git a/app/src/main/java/com/tpstreams/player/utils/NetworkUtils.kt b/app/src/main/java/com/tpstreams/player/utils/NetworkUtils.kt new file mode 100644 index 00000000..f333e258 --- /dev/null +++ b/app/src/main/java/com/tpstreams/player/utils/NetworkUtils.kt @@ -0,0 +1,34 @@ +package com.tpstreams.player.utils + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities + +/** + * Utility class for network-related operations + */ +object NetworkUtils { + + /** + * Check if the device has an active internet connection + * + * @param context Application or activity context + * @return true if internet is available, false otherwise + */ + fun isNetworkAvailable(context: Context): Boolean { + val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val network = connectivityManager.activeNetwork ?: return false + val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false + return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + } + + /** + * Check if the device is in offline mode (no internet connection) + * + * @param context Application or activity context + * @return true if device is offline, false otherwise + */ + fun isOfflineMode(context: Context): Boolean { + return !isNetworkAvailable(context) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/rounded_button_background.xml b/app/src/main/res/drawable/rounded_button_background.xml new file mode 100644 index 00000000..d64b74a9 --- /dev/null +++ b/app/src/main/res/drawable/rounded_button_background.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_downloads.xml b/app/src/main/res/layout/activity_downloads.xml new file mode 100644 index 00000000..3cb10317 --- /dev/null +++ b/app/src/main/res/layout/activity_downloads.xml @@ -0,0 +1,31 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index ecd6e3fb..abd48746 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -4,16 +4,53 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" tools:context=".MainActivity"> - + app:layout_constraintEnd_toEndOf="parent"> + + + +