/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tv.testinput; import android.annotation.TargetApi; import android.content.ComponentName; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.media.PlaybackParams; import android.media.tv.TvContract; import android.media.tv.TvContract.Programs; import android.media.tv.TvContract.RecordedPrograms; import android.media.tv.TvInputManager; import android.media.tv.TvInputService; import android.media.tv.TvTrackInfo; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.Log; import android.view.KeyEvent; import android.view.Surface; import com.android.tv.input.TunerHelper; import com.android.tv.testing.ChannelInfo; import com.android.tv.testing.testinput.ChannelState; import java.util.Date; import java.util.concurrent.TimeUnit; /** * Simple TV input service which provides test channels. */ public class TestTvInputService extends TvInputService { private static final String TAG = "TestTvInputService"; private static final int REFRESH_DELAY_MS = 1000 / 5; private static final boolean DEBUG = false; // Consider the command delivering time from Live TV. private static final long MAX_COMMAND_DELAY = TimeUnit.SECONDS.toMillis(3); private final TestInputControl mBackend = TestInputControl.getInstance(); private TunerHelper mTunerHelper; public static String buildInputId(Context context) { return TvContract.buildInputId(new ComponentName(context, TestTvInputService.class)); } @Override public void onCreate() { super.onCreate(); mBackend.init(this, buildInputId(this)); mTunerHelper = new TunerHelper(getResources().getInteger(R.integer.tuner_count)); } @Override public Session onCreateSession(String inputId) { Log.v(TAG, "Creating session for " + inputId); // onCreateSession always succeeds because this session can be used to play the recorded // program. return new SimpleSessionImpl(this); } @TargetApi(Build.VERSION_CODES.N) @Override public RecordingSession onCreateRecordingSession(String inputId) { Log.v(TAG, "Creating recording session for " + inputId); if (!mTunerHelper.tunerAvailableForRecording()) { return null; } return new SimpleRecordingSessionImpl(this, inputId); } /** * Simple session implementation that just display some text. */ private class SimpleSessionImpl extends Session { private static final int MSG_SEEK = 1000; private static final int SEEK_DELAY_MS = 300; private final Paint mTextPaint = new Paint(); private final DrawRunnable mDrawRunnable = new DrawRunnable(); private Surface mSurface = null; private Uri mChannelUri = null; private ChannelInfo mChannel = null; private ChannelState mCurrentState = null; private String mCurrentVideoTrackId = null; private String mCurrentAudioTrackId = null; private long mRecordStartTimeMs; private long mPausedTimeMs; // The time in milliseconds when the current position is lastly updated. private long mLastCurrentPositionUpdateTimeMs; // The current playback position. private long mCurrentPositionMs; // The current playback speed rate. private float mSpeed; private final Handler mHandler = new Handler(Looper.myLooper()) { @Override public void handleMessage(Message msg) { if (msg.what == MSG_SEEK) { // Actually, this input doesn't play any videos, it just shows the image. // So we should simulate the playback here by changing the current playback // position periodically in order to test the time shift. // If the playback is paused, the current playback position doesn't need to be // changed. if (mPausedTimeMs == 0) { long currentTimeMs = System.currentTimeMillis(); mCurrentPositionMs += (long) ((currentTimeMs - mLastCurrentPositionUpdateTimeMs) * mSpeed); mCurrentPositionMs = Math.max(mRecordStartTimeMs, Math.min(mCurrentPositionMs, currentTimeMs)); mLastCurrentPositionUpdateTimeMs = currentTimeMs; } sendEmptyMessageDelayed(MSG_SEEK, SEEK_DELAY_MS); } super.handleMessage(msg); } }; SimpleSessionImpl(Context context) { super(context); mTextPaint.setColor(Color.BLACK); mTextPaint.setTextSize(150); mHandler.post(mDrawRunnable); if (DEBUG) { Log.v(TAG, "Created session " + this); } } private void setAudioTrack(String selectedAudioTrackId) { Log.i(TAG, "Set audio track to " + selectedAudioTrackId); mCurrentAudioTrackId = selectedAudioTrackId; notifyTrackSelected(TvTrackInfo.TYPE_AUDIO, mCurrentAudioTrackId); } private void setVideoTrack(String selectedVideoTrackId) { Log.i(TAG, "Set video track to " + selectedVideoTrackId); mCurrentVideoTrackId = selectedVideoTrackId; notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, mCurrentVideoTrackId); } @Override public void onRelease() { if (DEBUG) { Log.v(TAG, "Releasing session " + this); } mTunerHelper.stopTune(mChannelUri); mDrawRunnable.cancel(); mHandler.removeCallbacks(mDrawRunnable); mSurface = null; mChannelUri = null; mChannel = null; mCurrentState = null; } @Override public boolean onSetSurface(Surface surface) { synchronized (mDrawRunnable) { mSurface = surface; } if (surface != null) { if (DEBUG) { Log.v(TAG, "Surface set"); } } else { if (DEBUG) { Log.v(TAG, "Surface unset"); } } return true; } @Override public void onSurfaceChanged(int format, int width, int height) { super.onSurfaceChanged(format, width, height); Log.d(TAG, "format=" + format + " width=" + width + " height=" + height); } @Override public void onSetStreamVolume(float volume) { // No-op } @Override public boolean onTune(Uri channelUri) { Log.i(TAG, "Tune to " + channelUri); mTunerHelper.stopTune(mChannelUri); mChannelUri = channelUri; ChannelInfo info = mBackend.getChannelInfo(channelUri); synchronized (mDrawRunnable) { if (info == null || mChannel == null || mChannel.originalNetworkId != info.originalNetworkId) { mCurrentState = null; } mChannel = info; mCurrentVideoTrackId = null; mCurrentAudioTrackId = null; } if (mChannel == null) { Log.i(TAG, "Channel not found for " + channelUri); notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); } else if (!mTunerHelper.tune(channelUri, false)) { Log.i(TAG, "No available tuner for " + channelUri); notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); } else { Log.i(TAG, "Tuning to " + mChannel); } notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_AVAILABLE); mRecordStartTimeMs = mCurrentPositionMs = mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis(); mPausedTimeMs = 0; mHandler.sendEmptyMessageDelayed(MSG_SEEK, SEEK_DELAY_MS); mSpeed = 1; return true; } @Override public void onSetCaptionEnabled(boolean enabled) { // No-op } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { Log.d(TAG, "onKeyDown (keyCode=" + keyCode + ", event=" + event + ")"); return true; } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { Log.d(TAG, "onKeyUp (keyCode=" + keyCode + ", event=" + event + ")"); return true; } @Override public long onTimeShiftGetCurrentPosition() { Log.d(TAG, "currentPositionMs=" + mCurrentPositionMs); return mCurrentPositionMs; } @Override public long onTimeShiftGetStartPosition() { return mRecordStartTimeMs; } @Override public void onTimeShiftPause() { mCurrentPositionMs = mPausedTimeMs = mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis(); } @Override public void onTimeShiftResume() { mSpeed = 1; mPausedTimeMs = 0; mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis(); } @Override public void onTimeShiftSeekTo(long timeMs) { mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis(); mCurrentPositionMs = Math.max(mRecordStartTimeMs, Math.min(timeMs, mLastCurrentPositionUpdateTimeMs)); } @Override public void onTimeShiftSetPlaybackParams(PlaybackParams params) { mSpeed = params.getSpeed(); } private final class DrawRunnable implements Runnable { private volatile boolean mIsCanceled = false; @Override public void run() { if (mIsCanceled) { return; } if (DEBUG) { Log.v(TAG, "Draw task running"); } boolean updatedState = false; ChannelState oldState; ChannelState newState = null; Surface currentSurface; ChannelInfo currentChannel; synchronized (this) { oldState = mCurrentState; currentSurface = mSurface; currentChannel = mChannel; if (currentChannel != null) { newState = mBackend.getChannelState(currentChannel.originalNetworkId); if (oldState == null || newState.getVersion() > oldState.getVersion()) { mCurrentState = newState; updatedState = true; } } else { mCurrentState = null; } if (currentSurface != null) { String now = new Date(mCurrentPositionMs).toString(); String name = currentChannel == null ? "Null" : currentChannel.name; Canvas c = currentSurface.lockCanvas(null); c.drawColor(0xFF888888); c.drawText(name, 100f, 200f, mTextPaint); c.drawText(now, 100f, 400f, mTextPaint); currentSurface.unlockCanvasAndPost(c); if (DEBUG) { Log.v(TAG, "Post to canvas"); } } else { if (DEBUG) { Log.v(TAG, "No surface"); } } } if (updatedState) { update(oldState, newState, currentChannel); } if (!mIsCanceled) { mHandler.postDelayed(this, REFRESH_DELAY_MS); } } private void update(ChannelState oldState, ChannelState newState, ChannelInfo currentChannel) { Log.i(TAG, "Updating channel " + currentChannel.number + " state to " + newState); notifyTracksChanged(newState.getTrackInfoList()); if (oldState == null || oldState.getTuneStatus() != newState.getTuneStatus()) { if (newState.getTuneStatus() == ChannelState.TUNE_STATUS_VIDEO_AVAILABLE) { notifyVideoAvailable(); //TODO handle parental controls. notifyContentAllowed(); setAudioTrack(newState.getSelectedAudioTrackId()); setVideoTrack(newState.getSelectedVideoTrackId()); } else { notifyVideoUnavailable(newState.getTuneStatus()); } } } public void cancel() { mIsCanceled = true; } } } private class SimpleRecordingSessionImpl extends RecordingSession { private final String[] PROGRAM_PROJECTION = { Programs.COLUMN_TITLE, Programs.COLUMN_EPISODE_TITLE, Programs.COLUMN_SHORT_DESCRIPTION, Programs.COLUMN_POSTER_ART_URI, Programs.COLUMN_THUMBNAIL_URI, Programs.COLUMN_CANONICAL_GENRE, Programs.COLUMN_CONTENT_RATING, Programs.COLUMN_START_TIME_UTC_MILLIS, Programs.COLUMN_END_TIME_UTC_MILLIS, Programs.COLUMN_VIDEO_WIDTH, Programs.COLUMN_VIDEO_HEIGHT, Programs.COLUMN_SEASON_DISPLAY_NUMBER, Programs.COLUMN_SEASON_TITLE, Programs.COLUMN_EPISODE_DISPLAY_NUMBER, }; private final String mInputId; private long mStartTime; private long mEndTime; private Uri mChannelUri; private Uri mProgramHintUri; public SimpleRecordingSessionImpl(Context context, String inputId) { super(context); mInputId = inputId; } @Override public void onTune(Uri uri) { Log.i(TAG, "SimpleReccordingSesesionImpl: onTune()"); mTunerHelper.stopRecording(mChannelUri); mChannelUri = uri; ChannelInfo channel = mBackend.getChannelInfo(uri); if (channel == null) { notifyError(TvInputManager.RECORDING_ERROR_UNKNOWN); } else if (!mTunerHelper.tune(uri, true)) { notifyError(TvInputManager.RECORDING_ERROR_RESOURCE_BUSY); } else { notifyTuned(uri); } } @Override public void onStartRecording(Uri programHintUri) { Log.i(TAG, "SimpleReccordingSesesionImpl: onStartRecording()"); mStartTime = System.currentTimeMillis(); mProgramHintUri = programHintUri; } @Override public void onStopRecording() { Log.i(TAG, "SimpleReccordingSesesionImpl: onStopRecording()"); mEndTime = System.currentTimeMillis(); final long startTime = mStartTime; final long endTime = mEndTime; final Uri programHintUri = mProgramHintUri; final Uri channelUri = mChannelUri; new AsyncTask() { @Override protected Void doInBackground(Void... arg0) { long time = System.currentTimeMillis(); if (programHintUri != null) { // Retrieves program info from mProgramHintUri try (Cursor c = getContentResolver().query(programHintUri, PROGRAM_PROJECTION, null, null, null)) { if (c != null && c.getCount() > 0) { storeRecordedProgram(c, startTime, endTime); return null; } } catch (Exception e) { Log.w(TAG, "Error querying " + this, e); } } // Retrieves the current program try (Cursor c = getContentResolver().query( TvContract.buildProgramsUriForChannel(channelUri, startTime, endTime - startTime < MAX_COMMAND_DELAY ? startTime : endTime - MAX_COMMAND_DELAY), PROGRAM_PROJECTION, null, null, null)) { if (c != null && c.getCount() == 1) { storeRecordedProgram(c, startTime, endTime); return null; } } catch (Exception e) { Log.w(TAG, "Error querying " + this, e); } storeRecordedProgram(null, startTime, endTime); return null; } private void storeRecordedProgram(Cursor c, long startTime, long endTime) { ContentValues values = new ContentValues(); values.put(RecordedPrograms.COLUMN_INPUT_ID, mInputId); values.put(RecordedPrograms.COLUMN_CHANNEL_ID, ContentUris.parseId(channelUri)); values.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, endTime - startTime); if (c != null) { int index = 0; c.moveToNext(); values.put(Programs.COLUMN_TITLE, c.getString(index++)); values.put(Programs.COLUMN_EPISODE_TITLE, c.getString(index++)); values.put(Programs.COLUMN_SHORT_DESCRIPTION, c.getString(index++)); values.put(Programs.COLUMN_POSTER_ART_URI, c.getString(index++)); values.put(Programs.COLUMN_THUMBNAIL_URI, c.getString(index++)); values.put(Programs.COLUMN_CANONICAL_GENRE, c.getString(index++)); values.put(Programs.COLUMN_CONTENT_RATING, c.getString(index++)); values.put(Programs.COLUMN_START_TIME_UTC_MILLIS, c.getLong(index++)); values.put(Programs.COLUMN_END_TIME_UTC_MILLIS, c.getLong(index++)); values.put(Programs.COLUMN_VIDEO_WIDTH, c.getLong(index++)); values.put(Programs.COLUMN_VIDEO_HEIGHT, c.getLong(index++)); values.put(Programs.COLUMN_SEASON_DISPLAY_NUMBER, c.getString(index++)); values.put(Programs.COLUMN_SEASON_TITLE, c.getString(index++)); values.put(Programs.COLUMN_EPISODE_DISPLAY_NUMBER, c.getString(index++)); } else { values.put(RecordedPrograms.COLUMN_TITLE, "No program info"); values.put(RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, startTime); values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime); } Uri uri = getContentResolver() .insert(TvContract.RecordedPrograms.CONTENT_URI, values); notifyRecordingStopped(uri); } }.execute(); } @Override public void onRelease() { Log.i(TAG, "SimpleReccordingSesesionImpl: onRelease()"); mTunerHelper.stopRecording(mChannelUri); mChannelUri = null; } } }