package com.felkertech.cumulustv.tv; import android.annotation.TargetApi; import android.content.ComponentName; import android.content.Context; import android.graphics.Bitmap; import android.media.tv.TvContentRating; import android.media.tv.TvInputManager; import android.media.tv.TvInputService; import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.v7.graphics.Palette; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.accessibility.CaptioningManager; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import com.bumptech.glide.Glide; import com.bumptech.glide.request.target.Target; import com.felkertech.cumulustv.model.ChannelDatabase; import com.felkertech.cumulustv.model.JsonChannel; import com.felkertech.cumulustv.player.CumulusTvPlayer; import com.felkertech.cumulustv.player.CumulusWebPlayer; import com.felkertech.cumulustv.player.MediaSourceFactory; import com.felkertech.cumulustv.services.CumulusJobService; import com.felkertech.n.cumulustv.R; import com.google.android.media.tv.companionlibrary.BaseTvInputService; import com.google.android.media.tv.companionlibrary.TvPlayer; import com.google.android.media.tv.companionlibrary.model.Advertisement; import com.google.android.media.tv.companionlibrary.model.Program; import com.google.android.media.tv.companionlibrary.model.RecordedProgram; import com.google.android.media.tv.companionlibrary.utils.TvContractUtils; import com.pnikosis.materialishprogress.ProgressWheel; import java.io.IOException; import java.util.concurrent.ExecutionException; /** * An instance of {@link BaseTvInputService} which plays Cumulus Tv videos. */ public class CumulusTvTifService extends BaseTvInputService { private static final String TAG = CumulusTvTifService.class.getSimpleName(); private static final boolean DEBUG = false; private static final long EPG_SYNC_DELAYED_PERIOD_MS = 1000 * 2; // 2 Seconds private CaptioningManager mCaptioningManager; @Override public void onCreate() { super.onCreate(); mCaptioningManager = (CaptioningManager) getSystemService(Context.CAPTIONING_SERVICE); } @Override public final Session onCreateSession(String inputId) { RichTvInputSessionImpl session = new RichTvInputSessionImpl(this, inputId); session.setOverlayViewEnabled(true); return super.sessionCreated(session); } @RequiresApi(api = Build.VERSION_CODES.N) @Nullable @Override public TvInputService.RecordingSession onCreateRecordingSession(String inputId) { return null; } class RichTvInputSessionImpl extends BaseTvInputService.Session { private static final float CAPTION_LINE_HEIGHT_RATIO = 0.0533f; private static final int TEXT_UNIT_PIXELS = 0; private static final String UNKNOWN_LANGUAGE = "und"; private int mSelectedSubtitleTrackIndex; private CumulusTvPlayer mPlayer; private boolean mCaptionEnabled; private String mInputId; private Context mContext; private boolean stillTuning; private JsonChannel jsonChannel; private long tuneTime; private boolean isWeb; RichTvInputSessionImpl(Context context, String inputId) { super(context, inputId); mCaptionEnabled = mCaptioningManager.isEnabled(); mContext = context; mInputId = inputId; } @Override public View onCreateOverlayView() { Log.d(TAG, "Create overlay view"); LayoutInflater inflater = (LayoutInflater) getApplicationContext() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); try { final View v = inflater.inflate(R.layout.loading, null); if(!stillTuning && jsonChannel.isAudioOnly()) { if (DEBUG) { Log.d(TAG, "Audio-only stream, show a foreground"); } ((TextView) v.findViewById(R.id.channel_msg)).setText(R.string.streaming_audio); } if (DEBUG) { Log.d(TAG, "Trying to load some visual display"); } if (jsonChannel == null) { if (DEBUG) { Log.w(TAG, "Cannot find channel"); } ((TextView) v.findViewById(R.id.channel)).setText(""); ((TextView) v.findViewById(R.id.title)).setText(""); } else if (isWeb) { CumulusWebPlayer wv = new CumulusWebPlayer(getApplicationContext(), new CumulusWebPlayer.WebViewListener() { @Override public void onPageFinished() { //Don't do anything } }); wv.load(jsonChannel.getMediaUrl()); return wv; } else if (jsonChannel.hasSplashscreen()) { if (DEBUG) { Log.d(TAG, "User supplied splashscreen"); } ImageView iv = new ImageView(getApplicationContext()); Glide.with(getApplicationContext()).load(jsonChannel.getSplashscreen()).into(iv); return iv; } else { if (DEBUG) { Log.d(TAG, "Manually create a splashscreen"); } ((TextView) v.findViewById(R.id.channel)).setText(jsonChannel.getNumber()); ((TextView) v.findViewById(R.id.title)).setText(jsonChannel.getName()); if (!jsonChannel.getLogo().isEmpty()) { final Bitmap[] bitmap = {null}; new Thread(new Runnable() { @Override public void run() { Handler h = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { super.handleMessage(msg); ((ImageView) v.findViewById(R.id.thumnail)) .setImageBitmap(bitmap[0]); //Use Palette to grab colors Palette p = Palette.from(bitmap[0]) .generate(); if (p.getVibrantSwatch() != null) { Log.d(TAG, "Use vibrant"); Palette.Swatch s = p.getVibrantSwatch(); v.setBackgroundColor(s.getRgb()); ((TextView) v.findViewById(R.id.channel)) .setTextColor(s.getTitleTextColor()); ((TextView) v.findViewById(R.id.title)) .setTextColor(s.getTitleTextColor()); ((TextView) v.findViewById(R.id.channel_msg)) .setTextColor(s.getTitleTextColor()); //Now style the progress bar if (p.getDarkVibrantSwatch() != null) { Palette.Swatch dvs = p.getDarkVibrantSwatch(); ((ProgressWheel) v.findViewById( R.id.indeterminate_progress_large_library)) .setBarColor(dvs.getRgb()); } } else if (p.getDarkVibrantSwatch() != null) { Log.d(TAG, "Use dark vibrant"); Palette.Swatch s = p.getDarkVibrantSwatch(); v.setBackgroundColor(s.getRgb()); ((TextView) v.findViewById(R.id.channel)) .setTextColor(s.getTitleTextColor()); ((TextView) v.findViewById(R.id.title)) .setTextColor(s.getTitleTextColor()); ((TextView) v.findViewById(R.id.channel_msg)) .setTextColor(s.getTitleTextColor()); ((ProgressWheel) v.findViewById( R.id.indeterminate_progress_large_library)) .setBarColor(s.getRgb()); } else if (p.getSwatches().size() > 0) { // Go with default if no vibrant swatch exists if (DEBUG) { Log.d(TAG, "No vibrant swatch, " + p.getSwatches().size() + " others"); } Palette.Swatch s = p.getSwatches().get(0); v.setBackgroundColor(s.getRgb()); ((TextView) v.findViewById(R.id.channel)) .setTextColor(s.getTitleTextColor()); ((TextView) v.findViewById(R.id.title)) .setTextColor(s.getTitleTextColor()); ((TextView) v.findViewById(R.id.channel_msg)) .setTextColor(s.getTitleTextColor()); ((ProgressWheel) v.findViewById( R.id.indeterminate_progress_large_library)) .setBarColor(s.getBodyTextColor()); } } }; try { bitmap[0] = Glide.with(getApplicationContext()) .load(ChannelDatabase.getNonNullChannelLogo(jsonChannel)) .asBitmap() .into(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) .get(); h.sendEmptyMessage(0); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } }).start(); } } if (DEBUG) { Log.d(TAG, "Overlay " + v.toString()); } return v; } catch (Exception e) { if (DEBUG) { Log.d(TAG, "Failure to open: " + e.getMessage()); e.printStackTrace(); } return null; } } @TargetApi(Build.VERSION_CODES.M) @RequiresApi(api = Build.VERSION_CODES.M) @Override public long onTimeShiftGetCurrentPosition() { if (mPlayer == null) { return TvInputManager.TIME_SHIFT_INVALID_TIME; } long currentMs = tuneTime + mPlayer.getCurrentPosition(); if (DEBUG) { Log.d(TAG, currentMs + " " + onTimeShiftGetStartPosition() + " start position"); Log.d(TAG, (currentMs - onTimeShiftGetStartPosition()) + " diff start position"); } return currentMs; } @Override public boolean onPlayProgram(Program program, long startPosMs) { if (program == null) { requestEpgSync(getCurrentChannelUri()); notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING); return false; } jsonChannel = ChannelDatabase.getInstance(getApplicationContext()).findChannelByMediaUrl( program.getInternalProviderData().getVideoUrl()); Log.d(TAG, "Play program " + program.getTitle() + " " + program.getInternalProviderData().getVideoUrl()); if (program.getInternalProviderData().getVideoUrl() == null) { Toast.makeText(mContext, getString(R.string.msg_no_url_found), Toast.LENGTH_SHORT).show(); notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); return false; } else { createPlayer(program.getInternalProviderData().getVideoType(), Uri.parse(program.getInternalProviderData().getVideoUrl())); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_AVAILABLE); } mPlayer.play(); notifyVideoAvailable(); Log.d(TAG, "The video should start playing"); return true; } } @RequiresApi(api = Build.VERSION_CODES.N) public boolean onPlayRecordedProgram(RecordedProgram recordedProgram) { createPlayer(recordedProgram.getInternalProviderData().getVideoType(), Uri.parse(recordedProgram.getInternalProviderData().getVideoUrl())); long recordingStartTime = recordedProgram.getInternalProviderData() .getRecordedProgramStartTime(); mPlayer.seekTo(recordingStartTime - recordedProgram.getStartTimeUtcMillis()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_AVAILABLE); } mPlayer.play(); notifyVideoAvailable(); return true; } @Override public long onTimeShiftGetStartPosition() { return tuneTime; } public TvPlayer getTvPlayer() { return mPlayer; } @Override public boolean onTune(Uri channelUri) { if (DEBUG) { Log.d(TAG, "Tune to " + channelUri.toString()); } notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING); releasePlayer(); tuneTime = System.currentTimeMillis(); stillTuning = true; // Update our channel for (String mediaUrl : ChannelDatabase.getInstance(mContext).getHashMap().keySet()) { if (ChannelDatabase.getInstance(mContext).getHashMap().get(mediaUrl) == Long.parseLong(channelUri.getLastPathSegment())) { jsonChannel = ChannelDatabase.getInstance(mContext) .findChannelByMediaUrl(mediaUrl); } } notifyVideoAvailable(); setOverlayViewEnabled(false); setOverlayViewEnabled(true); return super.onTune(channelUri); } @Override public void onSetCaptionEnabled(boolean enabled) { // Captions currently unsupported } @Override public void onPlayAdvertisement(Advertisement advertisement) { createPlayer(TvContractUtils.SOURCE_TYPE_HTTP_PROGRESSIVE, Uri.parse(advertisement.getRequestUrl())); } private void createPlayer(int videoType, Uri videoUrl) { releasePlayer(); mPlayer = new CumulusTvPlayer(mContext); mPlayer.registerCallback(new TvPlayer.Callback() { @Override public void onStarted() { super.onStarted(); Log.d(TAG, "Video available"); stillTuning = false; notifyVideoAvailable(); setOverlayViewEnabled(false); if (jsonChannel != null && jsonChannel.isAudioOnly()) { setOverlayViewEnabled(true); } } }); mPlayer.registerErrorListener(new CumulusTvPlayer.ErrorListener() { @Override public void onError(Exception error) { Log.e(TAG, error.getClass().getSimpleName() + " " + error.getMessage()); if (error instanceof MediaSourceFactory.NotMediaException) { isWeb = true; setOverlayViewEnabled(false); setOverlayViewEnabled(true); } } }); Log.d(TAG, "Create player for " + videoUrl); mPlayer.startPlaying(videoUrl); } private void releasePlayer() { if (mPlayer != null) { mPlayer.setSurface(null); mPlayer.stop(); mPlayer.release(); mPlayer = null; } } @Override public void onRelease() { super.onRelease(); releasePlayer(); } @Override public void onBlockContent(TvContentRating rating) { super.onBlockContent(rating); releasePlayer(); } private void requestEpgSync(final Uri channelUri) { CumulusJobService.requestImmediateSync1(CumulusTvTifService.this, mInputId, CumulusJobService.DEFAULT_IMMEDIATE_EPG_DURATION_MILLIS, new ComponentName(CumulusTvTifService.this, CumulusJobService.class)); new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { @Override public void run() { onTune(channelUri); } }, EPG_SYNC_DELAYED_PERIOD_MS); } } }