1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tv.testinput; 18 19 import android.content.ComponentName; 20 import android.content.Context; 21 import android.graphics.Canvas; 22 import android.graphics.Color; 23 import android.graphics.Paint; 24 import android.media.PlaybackParams; 25 import android.media.tv.TvContract; 26 import android.media.tv.TvInputManager; 27 import android.media.tv.TvInputService; 28 import android.media.tv.TvTrackInfo; 29 import android.net.Uri; 30 import android.os.Build; 31 import android.os.Handler; 32 import android.os.Looper; 33 import android.os.Message; 34 import android.util.Log; 35 import android.view.KeyEvent; 36 import android.view.Surface; 37 38 import com.android.tv.testing.ChannelInfo; 39 import com.android.tv.testing.testinput.ChannelState; 40 41 import java.util.Date; 42 43 /** 44 * Simple TV input service which provides test channels. 45 */ 46 public class TestTvInputService extends TvInputService { 47 private static final String TAG = "TestTvInputServices"; 48 private static final int REFRESH_DELAY_MS = 1000 / 5; 49 private static final boolean DEBUG = false; 50 private static final boolean HAS_TIME_SHIFT_API = Build.VERSION.SDK_INT 51 >= Build.VERSION_CODES.M; 52 private final TestInputControl mBackend = TestInputControl.getInstance(); 53 54 public static String buildInputId(Context context) { 55 return TvContract.buildInputId(new ComponentName(context, TestTvInputService.class)); 56 } 57 58 @Override 59 public void onCreate() { 60 super.onCreate(); 61 mBackend.init(this, buildInputId(this)); 62 } 63 64 @Override 65 public Session onCreateSession(String inputId) { 66 Log.v(TAG, "Creating session for " + inputId); 67 return new SimpleSessionImpl(this); 68 } 69 70 /** 71 * Simple session implementation that just display some text. 72 */ 73 private class SimpleSessionImpl extends Session { 74 private static final int MSG_SEEK = 1000; 75 private static final int SEEK_DELAY_MS = 300; 76 77 private final Paint mTextPaint = new Paint(); 78 private final DrawRunnable mDrawRunnable = new DrawRunnable(); 79 private Surface mSurface = null; 80 private ChannelInfo mChannel = null; 81 private ChannelState mCurrentState = null; 82 private String mCurrentVideoTrackId = null; 83 private String mCurrentAudioTrackId = null; 84 85 private long mRecordStartTimeMs; 86 private long mPausedTimeMs; 87 // The time in milliseconds when the current position is lastly updated. 88 private long mLastCurrentPositionUpdateTimeMs; 89 // The current playback position. 90 private long mCurrentPositionMs; 91 // The current playback speed rate. 92 private float mSpeed; 93 94 private final Handler mHandler = new Handler(Looper.myLooper()) { 95 @Override 96 public void handleMessage(Message msg) { 97 if (msg.what == MSG_SEEK) { 98 // Actually, this input doesn't play any videos, it just shows the image. 99 // So we should simulate the playback here by changing the current playback 100 // position periodically in order to test the time shift. 101 // If the playback is paused, the current playback position doesn't need to be 102 // changed. 103 if (mPausedTimeMs == 0) { 104 long currentTimeMs = System.currentTimeMillis(); 105 mCurrentPositionMs += (long) ((currentTimeMs 106 - mLastCurrentPositionUpdateTimeMs) * mSpeed); 107 mCurrentPositionMs = Math.max(mRecordStartTimeMs, 108 Math.min(mCurrentPositionMs, currentTimeMs)); 109 mLastCurrentPositionUpdateTimeMs = currentTimeMs; 110 } 111 sendEmptyMessageDelayed(MSG_SEEK, SEEK_DELAY_MS); 112 } 113 super.handleMessage(msg); 114 } 115 }; 116 117 SimpleSessionImpl(Context context) { 118 super(context); 119 mTextPaint.setColor(Color.BLACK); 120 mTextPaint.setTextSize(150); 121 mHandler.post(mDrawRunnable); 122 if (DEBUG) { 123 Log.v(TAG, "Created session " + this); 124 } 125 } 126 127 private void setAudioTrack(String selectedAudioTrackId) { 128 Log.i(TAG, "Set audio track to " + selectedAudioTrackId); 129 mCurrentAudioTrackId = selectedAudioTrackId; 130 notifyTrackSelected(TvTrackInfo.TYPE_AUDIO, mCurrentAudioTrackId); 131 } 132 133 private void setVideoTrack(String selectedVideoTrackId) { 134 Log.i(TAG, "Set video track to " + selectedVideoTrackId); 135 mCurrentVideoTrackId = selectedVideoTrackId; 136 notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, mCurrentVideoTrackId); 137 } 138 139 @Override 140 public void onRelease() { 141 if (DEBUG) { 142 Log.v(TAG, "Releasing session " + this); 143 } 144 mDrawRunnable.cancel(); 145 mHandler.removeCallbacks(mDrawRunnable); 146 mSurface = null; 147 mChannel = null; 148 mCurrentState = null; 149 } 150 151 @Override 152 public boolean onSetSurface(Surface surface) { 153 synchronized (mDrawRunnable) { 154 mSurface = surface; 155 } 156 if (surface != null) { 157 if (DEBUG) { 158 Log.v(TAG, "Surface set"); 159 } 160 } else { 161 if (DEBUG) { 162 Log.v(TAG, "Surface unset"); 163 } 164 } 165 166 return true; 167 } 168 169 @Override 170 public void onSurfaceChanged(int format, int width, int height) { 171 super.onSurfaceChanged(format, width, height); 172 Log.d(TAG, "format=" + format + " width=" + width + " height=" + height); 173 } 174 175 @Override 176 public void onSetStreamVolume(float volume) { 177 // No-op 178 } 179 180 @Override 181 public boolean onTune(Uri channelUri) { 182 Log.i(TAG, "Tune to " + channelUri); 183 ChannelInfo info = mBackend.getChannelInfo(channelUri); 184 synchronized (mDrawRunnable) { 185 if (info == null || mChannel == null 186 || mChannel.originalNetworkId != info.originalNetworkId) { 187 mCurrentState = null; 188 } 189 mChannel = info; 190 mCurrentVideoTrackId = null; 191 mCurrentAudioTrackId = null; 192 } 193 if (mChannel == null) { 194 Log.i(TAG, "Channel not found for " + channelUri); 195 notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); 196 } else { 197 Log.i(TAG, "Tuning to " + mChannel); 198 } 199 if (HAS_TIME_SHIFT_API) { 200 notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_AVAILABLE); 201 mRecordStartTimeMs = mCurrentPositionMs = mLastCurrentPositionUpdateTimeMs 202 = System.currentTimeMillis(); 203 mPausedTimeMs = 0; 204 mHandler.sendEmptyMessageDelayed(MSG_SEEK, SEEK_DELAY_MS); 205 mSpeed = 1; 206 } 207 return true; 208 } 209 210 @Override 211 public void onSetCaptionEnabled(boolean enabled) { 212 // No-op 213 } 214 215 @Override 216 public boolean onKeyDown(int keyCode, KeyEvent event) { 217 Log.d(TAG, "onKeyDown (keyCode=" + keyCode + ", event=" + event + ")"); 218 return true; 219 } 220 221 @Override 222 public boolean onKeyUp(int keyCode, KeyEvent event) { 223 Log.d(TAG, "onKeyUp (keyCode=" + keyCode + ", event=" + event + ")"); 224 return true; 225 } 226 227 @Override 228 public long onTimeShiftGetCurrentPosition() { 229 Log.d(TAG, "currentPositionMs=" + mCurrentPositionMs); 230 return mCurrentPositionMs; 231 } 232 233 @Override 234 public long onTimeShiftGetStartPosition() { 235 return mRecordStartTimeMs; 236 } 237 238 @Override 239 public void onTimeShiftPause() { 240 mCurrentPositionMs = mPausedTimeMs = mLastCurrentPositionUpdateTimeMs 241 = System.currentTimeMillis(); 242 } 243 244 @Override 245 public void onTimeShiftResume() { 246 mSpeed = 1; 247 mPausedTimeMs = 0; 248 mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis(); 249 } 250 251 @Override 252 public void onTimeShiftSeekTo(long timeMs) { 253 mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis(); 254 mCurrentPositionMs = Math.max(mRecordStartTimeMs, 255 Math.min(timeMs, mLastCurrentPositionUpdateTimeMs)); 256 } 257 258 @Override 259 public void onTimeShiftSetPlaybackParams(PlaybackParams params) { 260 mSpeed = params.getSpeed(); 261 } 262 263 private final class DrawRunnable implements Runnable { 264 private volatile boolean mIsCanceled = false; 265 266 @Override 267 public void run() { 268 if (mIsCanceled) { 269 return; 270 } 271 if (DEBUG) { 272 Log.v(TAG, "Draw task running"); 273 } 274 boolean updatedState = false; 275 ChannelState oldState; 276 ChannelState newState = null; 277 Surface currentSurface; 278 ChannelInfo currentChannel; 279 280 synchronized (this) { 281 oldState = mCurrentState; 282 currentSurface = mSurface; 283 currentChannel = mChannel; 284 if (currentChannel != null) { 285 newState = mBackend.getChannelState(currentChannel.originalNetworkId); 286 if (oldState == null || newState.getVersion() > oldState.getVersion()) { 287 mCurrentState = newState; 288 updatedState = true; 289 } 290 } else { 291 mCurrentState = null; 292 } 293 } 294 295 draw(currentSurface, currentChannel); 296 if (updatedState) { 297 update(oldState, newState, currentChannel); 298 } 299 300 if (!mIsCanceled) { 301 mHandler.postDelayed(this, REFRESH_DELAY_MS); 302 } 303 } 304 305 private void update(ChannelState oldState, ChannelState newState, 306 ChannelInfo currentChannel) { 307 Log.i(TAG, "Updating channel " + currentChannel.number + " state to " + newState); 308 notifyTracksChanged(newState.getTrackInfoList()); 309 if (oldState == null || oldState.getTuneStatus() != newState.getTuneStatus()) { 310 if (newState.getTuneStatus() == ChannelState.TUNE_STATUS_VIDEO_AVAILABLE) { 311 notifyVideoAvailable(); 312 //TODO handle parental controls. 313 notifyContentAllowed(); 314 setAudioTrack(newState.getSelectedAudioTrackId()); 315 setVideoTrack(newState.getSelectedVideoTrackId()); 316 } else { 317 notifyVideoUnavailable(newState.getTuneStatus()); 318 } 319 } 320 } 321 322 private void draw(Surface surface, ChannelInfo currentChannel) { 323 if (surface != null) { 324 String now = HAS_TIME_SHIFT_API 325 ? new Date(mCurrentPositionMs).toString() : new Date().toString(); 326 String name = currentChannel == null ? "Null" : currentChannel.name; 327 Canvas c = surface.lockCanvas(null); 328 c.drawColor(0xFF888888); 329 c.drawText(name, 100f, 200f, mTextPaint); 330 c.drawText(now, 100f, 400f, mTextPaint); 331 surface.unlockCanvasAndPost(c); 332 if (DEBUG) { 333 Log.v(TAG, "Post to canvas"); 334 } 335 } else { 336 if (DEBUG) { 337 Log.v(TAG, "No surface"); 338 } 339 } 340 } 341 342 public void cancel() { 343 mIsCanceled = true; 344 } 345 } 346 } 347 }