blob: 621ceacb97ae5bdb369131cee9a293e3da578334 [file] [log] [blame]
/*
* 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;
try {
Canvas c = currentSurface.lockCanvas(null);
c.drawColor(0xFF888888);
c.drawText(name, 100f, 200f, mTextPaint);
c.drawText(now, 100f, 400f, mTextPaint);
// Assuming c.drawXXX will never fail.
currentSurface.unlockCanvasAndPost(c);
} catch (IllegalArgumentException e) {
// The surface might have been abandoned. Ignore the exception.
}
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<Void, Void, Void>() {
@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;
}
}
}