package com.castlabs.sdk.downloads;

import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import androidx.annotation.NonNull;

import com.castlabs.android.player.DrmLicenseLoader;
import com.castlabs.android.player.TrickplayConfiguration;
import com.castlabs.android.player.exceptions.CastlabsPlayerException;
import com.castlabs.android.player.models.SideloadedTrack;
import com.google.android.material.snackbar.Snackbar;

import androidx.annotation.Nullable;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.TextView;

import com.castlabs.android.SdkConsts;
import com.castlabs.android.drm.Drm;
import com.castlabs.android.drm.DrmTodayConfiguration;
import com.castlabs.android.player.models.AudioTrack;
import com.castlabs.android.player.models.SubtitleTrack;
import com.castlabs.android.player.models.ThumbnailDataTrack;
import com.castlabs.android.player.models.VideoTrackQuality;
import com.castlabs.sdk.downloader.Download;
import com.castlabs.sdk.downloader.DownloadService;
import com.castlabs.sdk.downloader.DownloadServiceBinder;
import com.castlabs.sdk.downloader.Downloader;
import com.castlabs.sdk.downloader.MessageHandler;

import java.io.IOException;
import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {
	private static final String TAG = "MainActivity";
	/**
	 * List some content that we can download with this demo
	 */
	private static DownloadAsset[] DOWNLOAD_ASSETS = new DownloadAsset[]{
			new DownloadAsset("Simple Playback (Clear)", 1, "Plays an unencrypted stream using a PlayerView without any additional controls or callbacks.",
					new Util.BundleBuilder()
							.put(SdkConsts.INTENT_URL, "https://demo.cf.castlabs.com/media/TOS/abr/Manifest_clean_sizes.mpd")
							.put(SdkConsts.INTENT_SIDELOADED_TRACKS_ARRAYLIST, new ArrayList<SideloadedTrack>(){{
								// Bif Subtitles
								add(new SideloadedTrack.ThumbnailBuilder()
										.url("http://demo.castlabs.com/media/TOS/thumbs/thumbs.bif")
										.thumbnailType(ThumbnailDataTrack.TYPE_BIF)
										.get());
								// Vtt Subtitles
								//add(new SideloadedTrack.ThumbnailBuilder()
								//		.url("http://demo.castlabs.com/media/TOS/thumbs/thumbs.vtt")
								//		.thumbnailType(ThumbnailDataTrack.TYPE_WEBVTT_INDEX)
								//		.get());
							}})
							.get()),

			new DownloadAsset("Offline Key Storage (Widevine)", 2, "Plays stream protected with Widevine and stored the license on the device. The first " +
					"playback of this entry will fetch the licence, the second playback will use stored offline key.",
					new Util.BundleBuilder()
							.put(SdkConsts.INTENT_URL, "http://demo.castlabs.com/media/TOS/DASH/DEMO.mpd")
							.put(SdkConsts.INTENT_DRM_CONFIGURATION, new DrmTodayConfiguration.Builder(
									DrmTodayConfiguration.DRMTODAY_STAGING,
									"purchase",
									"sessionId",
									"six",
									"dasheverywhere_demo",
									Drm.Widevine)
									.offlineId("dasheverywhere_demo") // we use the asset ID as offline ID
									.get())
							.get()),
			new DownloadAsset("404 Error", 3, "Audio Track causes a 404 on download",
					new Util.BundleBuilder()
							.put(SdkConsts.INTENT_URL, "http://demo.cf.castlabs.com/media/bbb_abr/Manifest_404.mpd")
							.put(SdkConsts.INTENT_DRM_CONFIGURATION, new DrmTodayConfiguration.Builder(
									DrmTodayConfiguration.DRMTODAY_STAGING,
									"purchase",
									"sessionId",
									"client_dev",
									"BBB_TEST",
									Drm.Widevine)
									.offlineId("BBB_TEST") // we use the asset ID as offline ID
									.get())
							.get()),

			new DownloadAsset("Trick-Play Tracks", 4, "Downloads trick-play track besides the main video track",
					new Util.BundleBuilder()
							.put(SdkConsts.INTENT_URL, "https://demo.cf.castlabs.com/public/SUPPORT/TRAN-91/dash-trickplay/dash.mpd")
							.put(SdkConsts.INTENT_MERGE_VIDEO_TRACKS, true)
							.put(SdkConsts.INTENT_TRICKPLAY_CONFIGURATION, new TrickplayConfiguration
									.Builder()
									.preferTrickPlayTracks(true)
									.get())
							.put(SdkConsts.INTENT_DRM_CONFIGURATION, new DrmTodayConfiguration.Builder(
									DrmTodayConfiguration.DRMTODAY_STAGING,
									"purchase",
									"sessionId",
									"six",
									"",
									Drm.Widevine)
									.offlineId("dasheverywhere_demo")
									.get())
							.get()),
	};
	/**
	 * The download service binder that we need to connect to the download service
	 */
	private DownloadServiceBinder downloadServiceBinder;
	/**
	 * The data adapter for the list view
	 */
	private DownloadAssetsAdapter listAdapter;
	/**
	 * The broadcast receiver to get callbacks from the download service
	 */
	private BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
		@Override
		public void onReceive(Context context, Intent intent) {
			if (intent == null || intent.getAction() == null) {
			return;
		}

			Log.d(TAG, "Received Downloader Event: " + intent.getAction());
			if (listAdapter != null) {
				// notify a data change, which will trigger re-rendering
				// of the list view.
				// NOTE: This can be handled more efficiently if we only update
				// the entries that actually changed!
				listAdapter.notifyDataSetChanged();
			}
			// we handle some of the event actively here.
			switch (intent.getAction()) {
				case MessageHandler.ACTION_DOWNLOAD_STORAGE_LOW:
					showMessage("Not enough space to download. Pausing all downloads");
					break;
				case MessageHandler.ACTION_DOWNLOAD_ERROR:
					// We always get the download ID for errors
					String downloadId = intent.getStringExtra(MessageHandler.INTENT_DOWNLOAD_ID);
					// We should also get the exception message
					String errorMessage = intent.getStringExtra(MessageHandler.INTENT_DOWNLOAD_ERROR);
					Log.e(TAG, "Download error for #" + downloadId + ": " + errorMessage);

					// Some of the errors carry more information and we can try to access it here
					int errorType = intent.getIntExtra(
							MessageHandler.INTENT_DOWNLOAD_ERROR_TYPE, -1);

					Log.e(TAG, "Download error HTTP status code: " +
							intent.getIntExtra(
									MessageHandler.INTENT_DOWNLOAD_ERROR_HTTP_STATUS, -1));
					Log.e(TAG, "Download error type: " +
							errorType);
					Log.e(TAG, "Download error URL: " +
							intent.getStringExtra(MessageHandler.INTENT_DOWNLOAD_ERROR_HTTP_URL));

					// with the error type we can at least try to check for basic connectivity issues
					// In this example we do not actively check, but of course we could try to do
					// that here as well
					if (errorType == MessageHandler.ERROR_TYPE_CONNECTION_ERROR) {
						showMessage("Connectivity Issue. Please check your network connection " +
								"and re-try the download");
					} else if (errorType == MessageHandler.ERROR_TYPE_DRM_ERROR) {
						final int drmError = intent.getIntExtra(MessageHandler.INTENT_DOWNLOAD_ERROR_DRM, -1);
						Log.e(TAG, "DRM Error " + drmError);
						showMessage("DRM Error " + drmError + ": " + errorMessage);
					} else {
						showMessage("Error while downloading #" + downloadId + ": " + errorMessage);
					}
			}
		}
	};
	/**
	 * The service connection that essentially sets the binder
	 */
	private ServiceConnection downloadServiceConnection = new ServiceConnection() {
		public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
			downloadServiceBinder = (DownloadServiceBinder) iBinder;
			if (listAdapter != null) {
				// the renderer state depends on the binder, so we
				// issue a data set change here, which will cause
				// the list to be re-rendered.
				listAdapter.notifyDataSetChanged();
			}
		}

		public void onServiceDisconnected(ComponentName componentName) {
			downloadServiceBinder = null;
		}
	};

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		// Setup the list view
		RecyclerView recyclerView = findViewById(R.id.list);
		recyclerView.setLayoutManager(new LinearLayoutManager(this));
		listAdapter = new DownloadAssetsAdapter();
		recyclerView.setAdapter(listAdapter);

		// (Optional) Don't stop the service automatically. This implies that the service MUST be stopped
		// manually at some point. Disable if you need finer control over when the Service stops.
		// Read the Documentation for the autoStopService flag for more info
		//DownloadServiceBinder.autoStopService = false;

		// Check for permissions to write to the SD cards movies folder
		Util.checkAndAskPermissions(this);
	}

	@Override
	protected void onStart() {
		super.onStart();

		// We have to bind to the service using the ServiceConnection. The DownloadService will
		// automatically start and stop itself whenever necessary. You have to bind/unbind to the
		// service from each Activity you want to use it from.
		bindService(new Intent(this, DownloadService.class), downloadServiceConnection, Context.BIND_AUTO_CREATE);

		// Create and register an intent filter to get feedback for the downloader service while
		// it is running.
		IntentFilter filter = new IntentFilter();
		filter.addCategory(MessageHandler.INTENT_DOWNLOAD_CATEGORY);
		filter.addAction(MessageHandler.ACTION_DOWNLOAD_NO_PENDING);
		filter.addAction(MessageHandler.ACTION_DOWNLOAD_PROGRESS);
		filter.addAction(MessageHandler.ACTION_DOWNLOAD_COMPLETED);
		filter.addAction(MessageHandler.ACTION_DOWNLOAD_CREATED);
		filter.addAction(MessageHandler.ACTION_DOWNLOAD_DELETED);
		filter.addAction(MessageHandler.ACTION_DOWNLOAD_ERROR);
		filter.addAction(MessageHandler.ACTION_DOWNLOAD_STARTED);
		filter.addAction(MessageHandler.ACTION_DOWNLOAD_STOPPED);
		filter.addAction(MessageHandler.ACTION_DOWNLOAD_STORAGE_LOW);
		filter.addAction(MessageHandler.ACTION_DOWNLOAD_STORAGE_OK);
		filter.addAction(MessageHandler.ACTION_DOWNLOAD_PATH_UPDATE);
		filter.addAction(MessageHandler.ACTION_DOWNLOAD_SERVICE_TIMEOUT);

		LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, filter);

	}

	@Override
	protected void onStop() {
		// We disconnect from the download service and the local broadcaster when we
		// stop this activity
		try {
			unbindService(downloadServiceConnection);
		} catch (Exception e) {
			Log.e(TAG, "Error while unbinding download service", e);
		}
		// Unregister the local broadcast receiver since we are no longer interested in
		// receiving download events
		LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver);
		super.onStop();
	}

	// This is just here do handle permission requests on modern Android
	@Override
	public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
										   @NonNull int[] grantResults) {
		if (requestCode == Util.PERMISSION_REQUEST) {
			Util.checkAndAskPermissions(this);
		}
	}


	/**
	 * This action takes an asset and prepares (and eventually starts) a download.
	 * Note that preparing a download involves downloading the Manifest and is a async operation.
	 * You might want to show a progress indicator while the download is prepared.
	 *
	 * @param downloadAsset The asset
	 */
	private void prepareDownload(DownloadAsset downloadAsset) {
		if (downloadServiceBinder == null) {
			showMessage("Service binder not ready");
			return;
		}

		// Prepare a Bundle that we will use to prepare the download.
		// We are going to use the same bundle that we would use for
		// streaming playback, but we make sure that we add a
		// download ID and a target folder.
		//
		// Note: The folder is not generic but specific for this download
		Bundle bundle = downloadAsset.bundle;
		final String downloadId = String.valueOf(downloadAsset.id);
		bundle.putString(SdkConsts.INTENT_DOWNLOAD_ID, downloadId);
		bundle.putString(SdkConsts.INTENT_DOWNLOAD_FOLDER, Util.getDownloadFolder(this, downloadId));

		// We pass our bundle to the service binder. This will prepare the download, usually by
		// downloading the Manifest. Then the provided callback is triggered. In the callback
		// we receive the download model and can apply our track selection to decide
		// which track should actually be downloaded.
		// Note that at this point nothing is stored on disk or in the download cache. Once
		// we selected the tracks we want to download, we call "createDownload" to actually
		// create and store the download with our selection.
		downloadServiceBinder.prepareDownload(this, bundle, new Downloader.ModelReadyCallback() {
			@Override
			public void onError(@NonNull Exception e) {
				showMessage("Error while preparing download " + e.getMessage());
			}

			@Override
			public void onModelAvailable(@NonNull Download download) {
				// VIDEO TRACK SELECTION
				//
				// The qualities are sorted by their bitrate, the quality at index 0
				// is the quality with the highest bitrate.
				// This is the place where you can apply the video quality selection and
				// for instance selected specific SD or HD renditions.
				//
				// Note: The available qualities are already filtered by device capabilities.
				// The filtering can be controlled by bundle parameters passed when you prepare
				// the download the same way you would control that for streaming playback
				if (download.getVideoTrackQualities() != null) {
					Spinner videoSelectionSpinner = findViewById(R.id.video_selection);
					String selection = videoSelectionSpinner.getSelectedItem().toString();
					switch (selection) {
						case "None":
							// nothing to select
							break;
						case "Highest":
							// Select the highest available quality. This is the first quality in the
							// list
							download.setSelectedVideoTrackQuality(0);
							break;
						case "Lowest":
							// Select the lowest quality, which is the last quality in the list
							download.setSelectedVideoTrackQuality(download.getVideoTrackQualities().length - 1);
							break;
						case "HD":
							// Start iterating at the highest bitrate and select the quality that
							// is the first one that is HD
							for (int i = 0; i < download.getVideoTrackQualities().length; i++) {
								VideoTrackQuality quality = download.getVideoTrackQualities()[i];
								if (quality.getHeight() >= 720) {
									download.setSelectedVideoTrackQuality(i);
									break;
								}
							}
							if (download.getSelectedVideoTrackQuality() < 0) {
								// no matching HD quality was found so we fall back to the
								// highest quality
								download.setSelectedVideoTrackQuality(0);
							}
							break;
						case "SD":
							// Start iterating at the highest bitrate and select the quality that
							// is the first one that is SD, i.e less than 720p
							for (int i = 0; i < download.getVideoTrackQualities().length; i++) {
								VideoTrackQuality quality = download.getVideoTrackQualities()[i];
								if (quality.getHeight() < 720) {
									download.setSelectedVideoTrackQuality(i);
									break;
								}
							}
							if (download.getSelectedVideoTrackQuality() < 0) {
								// no matching HD quality was found so we fall back to the
								// lowest one
								download.setSelectedVideoTrackQuality(download.getVideoTrackQualities().length - 1);
							}
							break;
					}
				}

				// TRICK-PLAY TRACK SELECTION
				//
				// The selection is similar to main video track
				if (download.getTrickPlayTrackQualities() != null) {
					Spinner videoSelectionSpinner = findViewById(R.id.video_selection);
					String selection = videoSelectionSpinner.getSelectedItem().toString();
					switch (selection) {
						case "None":
							// nothing to select
							break;
						case "Highest":
							// Select the highest available quality. This is the first quality in the
							// list
							download.setSelectedTrickPlayTrackQuality(0);
							break;
						case "Lowest":
							// Select the lowest quality, which is the last quality in the list
							download.setSelectedTrickPlayTrackQuality(download.getTrickPlayTrackQualities().length - 1);
							break;
						case "HD":
							// Start iterating at the highest bitrate and select the quality that
							// is the first one that is HD
							for (int i = 0; i < download.getTrickPlayTrackQualities().length; i++) {
								VideoTrackQuality quality = download.getTrickPlayTrackQualities()[i];
								if (quality.getHeight() >= 720) {
									download.setSelectedTrickPlayTrackQuality(i);
									break;
								}
							}
							if (download.getSelectedTrickPlayTrackQuality() < 0) {
								// no matching HD quality was found so we fall back to the
								// highest quality
								download.setSelectedTrickPlayTrackQuality(0);
							}
							break;
						case "SD":
							// Start iterating at the highest bitrate and select the quality that
							// is the first one that is SD, i.e less than 720p
							for (int i = 0; i < download.getTrickPlayTrackQualities().length; i++) {
								VideoTrackQuality quality = download.getTrickPlayTrackQualities()[i];
								if (quality.getHeight() < 720) {
									download.setSelectedTrickPlayTrackQuality(i);
									break;
								}
							}
							if (download.getSelectedTrickPlayTrackQuality() < 0) {
								// no matching HD quality was found so we fall back to the
								// lowest one
								download.setSelectedTrickPlayTrackQuality(download.getTrickPlayTrackQualities().length - 1);
							}
							break;
					}
				}

				// AUDIO TRACK SELECTION
				//
				// Select a set of audio tracks
				if (download.getAudioTracks() != null) {
					Spinner audioSelectionSpinner = findViewById(R.id.audio_selection);
					String selection = audioSelectionSpinner.getSelectedItem().toString();
					switch (selection) {
						case "None":
							// nothing to select
							break;
						case "All":
							// Select all audio tracks
							int[] audioTracks = new int[download.getAudioTracks().length];
							for (int i = 0; i < audioTracks.length; i++) {
								audioTracks[i] = i;
							}
							download.setSelectedAudioTracks(audioTracks);
							break;
						case "English":
							// Select the first track that matches english
							for (int i = 0; i < download.getAudioTracks().length; i++) {
								AudioTrack audioTrack = download.getAudioTracks()[i];
								if ("en".equals(audioTrack.getLanguage())) {
									download.setSelectedAudioTracks(new int[]{i});
									break;
								}
							}
							break;
					}
				}

				// SUBTITLE TRACK SELECTION
				//
				// Select subtitle tracks
				if (download.getSubtitleTracks() != null) {
					Spinner textSelectionSpinner = findViewById(R.id.text_selection);
					String selection = textSelectionSpinner.getSelectedItem().toString();
					switch (selection) {
						case "None":
							// nothing to select
							break;
						case "All":
							// Select all audio tracks
							int[] textTracks = new int[download.getSubtitleTracks().length];
							for (int i = 0; i < textTracks.length; i++) {
								textTracks[i] = i;
							}
							download.setSelectedSubtitleTracks(textTracks);
							break;
						case "English":
							// Select the first track that matches english
							for (int i = 0; i < download.getSubtitleTracks().length; i++) {
								SubtitleTrack textTrack = download.getSubtitleTracks()[i];
								if ("en".equals(textTrack.getLanguage())) {
									download.setSelectedSubtitleTracks(new int[]{i});
									break;
								}
							}
							break;
					}

				}

				// Now that we selected the track we want to download, we call createDownload
				// on the service binder to persist the downlaod. We pass true as a second argument
				// to make sure that the created download is enqueued and started (if a slot is free)
				// immediately.
				try {
					downloadServiceBinder.createDownload(download, true);
				} catch (IOException e) {
					showMessage("Error while creating download: " + e.getMessage());
					Log.e(TAG, "Error while creating download: " + e.getMessage(), e);
				}
			}
		});
	}

	/**
	 * We bind to this method from UI components to start offline playback for a given asset
	 *
	 * @param item The asset to start
	 */
	private void playOfflineAction(DownloadAsset item) {
		if (downloadServiceBinder == null) {
			return;
		}
		String downloadId = Integer.toString(item.id);
		Download download = downloadServiceBinder.findDownloadById(downloadId);
		if (download == null) {
			return;
		}

		String folder = Util.getDownloadFolder(this, downloadId);
		// Extract needed info and build a bundle
		Intent intent = new Intent(this, PlaybackActivity.class);
		intent.putExtras(item.bundle);
		intent.putExtra(SdkConsts.INTENT_URL, download.getLocalManifestUrl());
		intent.putExtra(SdkConsts.INTENT_DRM_CONFIGURATION, download.getDrmConfiguration());
		intent.putExtra(SdkConsts.INTENT_DOWNLOAD_FOLDER, folder);
		// In order to play with Thumbnails, we need to add it once again to the playing intent,
		// event we're playing offline. If the side-loaded track was present when the downloade
		// was created, it will be available and the SDK will find it
		//
		// We only want to use the Thumbnail track for the first asset
		if (download.getRemoteUrl()
				.equals("http://demo.castlabs.com/media/TOS/abr/Manifest_clean_sizes.mpd")) {
			intent.putParcelableArrayListExtra(SdkConsts.INTENT_SIDELOADED_TRACKS_ARRAYLIST, new ArrayList<SideloadedTrack>(){{
				// Bif Subtitles
				add(new SideloadedTrack.ThumbnailBuilder()
						.url("http://demo.castlabs.com/media/TOS/thumbs/thumbs.bif")
						.thumbnailType(ThumbnailDataTrack.TYPE_BIF)
						.get());
				// Vtt Subtitles
				//add(new SideloadedTrack.ThumbnailBuilder()
				//		.url("http://demo.castlabs.com/media/TOS/thumbs/thumbs.vtt")
				//		.thumbnailType(ThumbnailDataTrack.TYPE_WEBVTT_INDEX)
				//		.get());
			}});
		}

		startActivity(intent);
	}

	/**
	 * We bind to this method from UI components to play an asset online.
	 *
	 * @param item The asset
	 */
	private void playOnlineAction(DownloadAsset item) {
		// just play the online stream
		startActivity(new Intent(this,
				PlaybackActivity.class).putExtras(item.bundle));
	}

	/**
	 * We bind to this method from UI components to change the current state for a downloadable
	 * asset and the underlying download.
	 * <p>
	 * If no download exists yet, we will create one. If the download exists already, we either
	 * pause or resume the download, based on its current state.
	 *
	 * @param item The asset
	 */
	private void performDownloadAction(DownloadAsset item) {
		if (downloadServiceBinder == null) {
			return;
		}
		String downloadId = Integer.toString(item.id);
		Download download = downloadServiceBinder.findDownloadById(downloadId);
		if (download == null) {
			// no download, so we create one
			prepareDownload(item);
		} else {
			switch (download.getState()) {
				case Download.STATE_DONE:
					// If the download is completed, we do not want to do anything here
					break;
				case Download.STATE_ERROR:
				case Download.STATE_IDLE:
					// In case of an error state or the downlaod is idle, we resume it
					downloadServiceBinder.resumeDownload(downloadId);
					break;
				case Download.STATE_QUEUED:
				case Download.STATE_LOADING:
					// in case the download is queued or currently loading, we pause it
					downloadServiceBinder.pauseDownload(downloadId);
					break;
			}
		}
	}

	private void deleteDownloadAction(DownloadAsset item) {
		if (downloadServiceBinder == null) {
			return;
		}
		String downloadId = Integer.toString(item.id);
		Download download = downloadServiceBinder.findDownloadById(downloadId);
		if (download != null) {
			@Nullable String offlineId = download.getDrmConfiguration() != null ? download.getDrmConfiguration().offlineId : null;
			downloadServiceBinder.deleteDownload(downloadId);
			if (offlineId != null) {
				DownloadServiceBinder.removeLicense(download, null, new DrmLicenseLoader.Callback() {
					@Override
					public void onError(CastlabsPlayerException e) {
						Log.d(TAG, "Error removing license: downloadId = " + downloadId + ", offlineId = " + offlineId + ", Error:  " + e);
					}

					@Override
					public void onLicenseLoaded() {
					}

					@Override
					public void onLicenseRemoved() {
						Log.d(TAG, "License removed: downloadId = " + downloadId + ", offlineId = " + offlineId);
					}
				});
			}
		}
	}

	private void showMessage(String s) {
		Snackbar.make(findViewById(R.id.container), s, Snackbar.LENGTH_LONG).show();
	}

	/**
	 * This is our simple date model that holds some very basic information about a downloadable
	 * asset. The most important part here is the reference to the bundle which contains the
	 * information used by the library as well as the download ID.
	 */
	static class DownloadAsset {
		public final String name;
		public final int id;
		public final String description;
		private final Bundle bundle;

		DownloadAsset(String name, int id, String description, Bundle bundle) {
			this.name = name;
			this.id = id;
			this.description = description;
			this.bundle = bundle;
		}
	}

	/**
	 * Simple adapter for our recycler view that operates on the static list of items.
	 */
	class DownloadAssetsAdapter extends RecyclerView.Adapter<DownloadAssetsAdapterViewHolder> {

		@Override
		public DownloadAssetsAdapterViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
			View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.demo_list_item, parent, false);
			return new DownloadAssetsAdapterViewHolder(view);
		}

		@Override
		public void onBindViewHolder(DownloadAssetsAdapterViewHolder holder, int position) {
			holder.bind(DOWNLOAD_ASSETS[position]);
		}

		@Override
		public int getItemCount() {
			return DOWNLOAD_ASSETS.length;
		}
	}

	/**
	 * This is a simple view holder that we use to bind the UI to the given asset.
	 * The holder delegates clicks and actions directly upstream and update the UI
	 * when bound according to the asset state
	 */
	class DownloadAssetsAdapterViewHolder extends RecyclerView.ViewHolder {
		private final TextView titleView;
		private final TextView descriptionView;
		private final Button playOnlineBtn;
		private final Button playOfflineBtn;
		private final Button downloadActionBtn;
		private final Button deleteBtn;
		private final TextView downloadState;
		private final ProgressBar progressBar;

		private DownloadAsset item;


		DownloadAssetsAdapterViewHolder(View view) {
			super(view);
			this.titleView = view.findViewById(R.id.title);
			this.descriptionView = view.findViewById(R.id.description);
			this.playOnlineBtn = view.findViewById(R.id.play_online);
			this.playOfflineBtn = view.findViewById(R.id.play_offline);
			this.downloadActionBtn = view.findViewById(R.id.download_action);
			this.deleteBtn = view.findViewById(R.id.delete);
			this.downloadState = view.findViewById(R.id.state);
			this.progressBar = view.findViewById(R.id.download_progress);

			// We can setup click listeners for our buttons
			// here since we are going to hold a reference to the
			// item when we are binding this view holder
			//
			// For this example, the view holder is an inner class
			// so we can delegate to the action methods outside of the view holder
			// directly
			playOnlineBtn.setOnClickListener(new View.OnClickListener() {
				@Override
				public void onClick(View view) {
					playOnlineAction(item);
				}
			});

			downloadActionBtn.setOnClickListener(new View.OnClickListener() {
				@Override
				public void onClick(View view) {
					performDownloadAction(item);
				}
			});

			deleteBtn.setOnClickListener(new View.OnClickListener() {
				@Override
				public void onClick(View view) {
					deleteDownloadAction(item);
				}
			});

			playOfflineBtn.setOnClickListener(new View.OnClickListener() {
				@Override
				public void onClick(View view) {
					playOfflineAction(item);
				}
			});
		}

		@SuppressLint("SetTextI18n")
		void bind(DownloadAsset item) {
			// Hold a reference to the item so the click listeners
			// can use it and we con't have to
			this.item = item;
			// The easy stuff first, we set the
			// name and the description
			titleView.setText(item.name);
			descriptionView.setText(item.description);

			if (downloadServiceBinder == null) {
				// we have no download service connected, so there is not much we can do and
				// we disable all download interactions
				playOfflineBtn.setEnabled(false);
				downloadActionBtn.setEnabled(false);
				deleteBtn.setEnabled(false);
				downloadState.setText("");
				progressBar.setProgress(0);
				progressBar.setEnabled(false);
			} else {
				// set the button state according to the current download state
				Download download = downloadServiceBinder.findDownloadById(Integer.toString(item.id));
				if (download == null) {
					// this download is unknown
					downloadState.setText("");
					playOfflineBtn.setEnabled(false);
					downloadActionBtn.setText("Download");
					downloadActionBtn.setEnabled(true);
					deleteBtn.setEnabled(false);
					progressBar.setProgress(0);
					progressBar.setEnabled(false);
				} else {
					// we have a download so we can always delete the download
					deleteBtn.setEnabled(true);

					// Calculate the download ratio.
					// Note that this is an estimate, so we make sure that we are
					// between 0 and 100 here. We also make sure to mark a completed
					// download as 100% below
					double downloadedRatio = (double) download.getDownloadedSize() / (double) download.getEstimatedSize();
					int progress = Math.max(0, Math.min(100, (int) (downloadedRatio * 100.0)));

					progressBar.setProgress(progress);
					progressBar.setEnabled(true);
					// set state based on download state
					switch (download.getState()) {
						case Download.STATE_DONE:
							downloadState.setText("Done");
							downloadActionBtn.setEnabled(false);
							downloadActionBtn.setText("Pause");
							// since the download rate is an estimation, we make sure we
							// mark it as 100% once the download is completed
							progressBar.setProgress(100);
							playOfflineBtn.setEnabled(true);
							break;
						case Download.STATE_IDLE:
							downloadState.setText("Paused");
							downloadActionBtn.setEnabled(true);
							downloadActionBtn.setText("Resume");
							playOfflineBtn.setEnabled(false);
							break;
						case Download.STATE_QUEUED:
							downloadState.setText("Queued");
							downloadActionBtn.setEnabled(true);
							downloadActionBtn.setText("Pause");
							playOfflineBtn.setEnabled(false);
							break;
						case Download.STATE_LOADING:
							downloadState.setText("Loading");
							downloadActionBtn.setEnabled(true);
							downloadActionBtn.setText("Pause");
							playOfflineBtn.setEnabled(false);
							break;
						case Download.STATE_ERROR:
							downloadState.setText("Error");
							downloadActionBtn.setEnabled(true);
							downloadActionBtn.setText("Retry");
							playOfflineBtn.setEnabled(false);
							break;
					}
				}
			}
		}
	}
}
