• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.annotation.TargetApi;
20 import android.content.ComponentName;
21 import android.content.ContentUris;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.database.Cursor;
25 import android.graphics.Canvas;
26 import android.graphics.Color;
27 import android.graphics.Paint;
28 import android.media.PlaybackParams;
29 import android.media.tv.TvContract;
30 import android.media.tv.TvContract.Programs;
31 import android.media.tv.TvContract.RecordedPrograms;
32 import android.media.tv.TvInputManager;
33 import android.media.tv.TvInputService;
34 import android.media.tv.TvTrackInfo;
35 import android.net.Uri;
36 import android.os.AsyncTask;
37 import android.os.Build;
38 import android.os.Handler;
39 import android.os.Looper;
40 import android.os.Message;
41 import android.util.Log;
42 import android.view.KeyEvent;
43 import android.view.Surface;
44 
45 import com.android.tv.input.TunerHelper;
46 import com.android.tv.testing.ChannelInfo;
47 import com.android.tv.testing.testinput.ChannelState;
48 
49 import java.util.Date;
50 import java.util.concurrent.TimeUnit;
51 
52 /**
53  * Simple TV input service which provides test channels.
54  */
55 public class TestTvInputService extends TvInputService {
56     private static final String TAG = "TestTvInputService";
57     private static final int REFRESH_DELAY_MS = 1000 / 5;
58     private static final boolean DEBUG = false;
59 
60     // Consider the command delivering time from Live TV.
61     private static final long MAX_COMMAND_DELAY = TimeUnit.SECONDS.toMillis(3);
62 
63     private final TestInputControl mBackend = TestInputControl.getInstance();
64 
65     private TunerHelper mTunerHelper;
66 
buildInputId(Context context)67     public static String buildInputId(Context context) {
68         return TvContract.buildInputId(new ComponentName(context, TestTvInputService.class));
69     }
70 
71     @Override
onCreate()72     public void onCreate() {
73         super.onCreate();
74         mBackend.init(this, buildInputId(this));
75         mTunerHelper = new TunerHelper(getResources().getInteger(R.integer.tuner_count));
76     }
77 
78     @Override
onCreateSession(String inputId)79     public Session onCreateSession(String inputId) {
80         Log.v(TAG, "Creating session for " + inputId);
81         // onCreateSession always succeeds because this session can be used to play the recorded
82         // program.
83         return new SimpleSessionImpl(this);
84     }
85 
86     @TargetApi(Build.VERSION_CODES.N)
87     @Override
onCreateRecordingSession(String inputId)88     public RecordingSession onCreateRecordingSession(String inputId) {
89         Log.v(TAG, "Creating recording session for " + inputId);
90         if (!mTunerHelper.tunerAvailableForRecording()) {
91             return null;
92         }
93         return new SimpleRecordingSessionImpl(this, inputId);
94     }
95 
96     /**
97      * Simple session implementation that just display some text.
98      */
99     private class SimpleSessionImpl extends Session {
100         private static final int MSG_SEEK = 1000;
101         private static final int SEEK_DELAY_MS = 300;
102 
103         private final Paint mTextPaint = new Paint();
104         private final DrawRunnable mDrawRunnable = new DrawRunnable();
105         private Surface mSurface = null;
106         private Uri mChannelUri = null;
107         private ChannelInfo mChannel = null;
108         private ChannelState mCurrentState = null;
109         private String mCurrentVideoTrackId = null;
110         private String mCurrentAudioTrackId = null;
111 
112         private long mRecordStartTimeMs;
113         private long mPausedTimeMs;
114         // The time in milliseconds when the current position is lastly updated.
115         private long mLastCurrentPositionUpdateTimeMs;
116         // The current playback position.
117         private long mCurrentPositionMs;
118         // The current playback speed rate.
119         private float mSpeed;
120 
121         private final Handler mHandler = new Handler(Looper.myLooper()) {
122             @Override
123             public void handleMessage(Message msg) {
124                 if (msg.what == MSG_SEEK) {
125                     // Actually, this input doesn't play any videos, it just shows the image.
126                     // So we should simulate the playback here by changing the current playback
127                     // position periodically in order to test the time shift.
128                     // If the playback is paused, the current playback position doesn't need to be
129                     // changed.
130                     if (mPausedTimeMs == 0) {
131                         long currentTimeMs = System.currentTimeMillis();
132                         mCurrentPositionMs += (long) ((currentTimeMs
133                                 - mLastCurrentPositionUpdateTimeMs) * mSpeed);
134                         mCurrentPositionMs = Math.max(mRecordStartTimeMs,
135                                 Math.min(mCurrentPositionMs, currentTimeMs));
136                         mLastCurrentPositionUpdateTimeMs = currentTimeMs;
137                     }
138                     sendEmptyMessageDelayed(MSG_SEEK, SEEK_DELAY_MS);
139                 }
140                 super.handleMessage(msg);
141             }
142         };
143 
SimpleSessionImpl(Context context)144         SimpleSessionImpl(Context context) {
145             super(context);
146             mTextPaint.setColor(Color.BLACK);
147             mTextPaint.setTextSize(150);
148             mHandler.post(mDrawRunnable);
149             if (DEBUG) {
150                 Log.v(TAG, "Created session " + this);
151             }
152         }
153 
setAudioTrack(String selectedAudioTrackId)154         private void setAudioTrack(String selectedAudioTrackId) {
155             Log.i(TAG, "Set audio track to " + selectedAudioTrackId);
156             mCurrentAudioTrackId = selectedAudioTrackId;
157             notifyTrackSelected(TvTrackInfo.TYPE_AUDIO, mCurrentAudioTrackId);
158         }
159 
setVideoTrack(String selectedVideoTrackId)160         private void setVideoTrack(String selectedVideoTrackId) {
161             Log.i(TAG, "Set video track to " + selectedVideoTrackId);
162             mCurrentVideoTrackId = selectedVideoTrackId;
163             notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, mCurrentVideoTrackId);
164         }
165 
166         @Override
onRelease()167         public void onRelease() {
168             if (DEBUG) {
169                 Log.v(TAG, "Releasing session " + this);
170             }
171             mTunerHelper.stopTune(mChannelUri);
172             mDrawRunnable.cancel();
173             mHandler.removeCallbacks(mDrawRunnable);
174             mSurface = null;
175             mChannelUri = null;
176             mChannel = null;
177             mCurrentState = null;
178         }
179 
180         @Override
onSetSurface(Surface surface)181         public boolean onSetSurface(Surface surface) {
182             synchronized (mDrawRunnable) {
183                 mSurface = surface;
184             }
185             if (surface != null) {
186                 if (DEBUG) {
187                     Log.v(TAG, "Surface set");
188                 }
189             } else {
190                 if (DEBUG) {
191                     Log.v(TAG, "Surface unset");
192                 }
193             }
194 
195             return true;
196         }
197 
198         @Override
onSurfaceChanged(int format, int width, int height)199         public void onSurfaceChanged(int format, int width, int height) {
200             super.onSurfaceChanged(format, width, height);
201             Log.d(TAG, "format=" + format + " width=" + width + " height=" + height);
202         }
203 
204         @Override
onSetStreamVolume(float volume)205         public void onSetStreamVolume(float volume) {
206             // No-op
207         }
208 
209         @Override
onTune(Uri channelUri)210         public boolean onTune(Uri channelUri) {
211             Log.i(TAG, "Tune to " + channelUri);
212             mTunerHelper.stopTune(mChannelUri);
213             mChannelUri = channelUri;
214             ChannelInfo info = mBackend.getChannelInfo(channelUri);
215             synchronized (mDrawRunnable) {
216                 if (info == null || mChannel == null
217                         || mChannel.originalNetworkId != info.originalNetworkId) {
218                     mCurrentState = null;
219                 }
220                 mChannel = info;
221                 mCurrentVideoTrackId = null;
222                 mCurrentAudioTrackId = null;
223             }
224             if (mChannel == null) {
225                 Log.i(TAG, "Channel not found for " + channelUri);
226                 notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
227             } else if (!mTunerHelper.tune(channelUri, false)) {
228                 Log.i(TAG, "No available tuner for " + channelUri);
229                 notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
230             } else {
231                 Log.i(TAG, "Tuning to " + mChannel);
232             }
233             notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_AVAILABLE);
234             mRecordStartTimeMs = mCurrentPositionMs = mLastCurrentPositionUpdateTimeMs
235                     = System.currentTimeMillis();
236             mPausedTimeMs = 0;
237             mHandler.sendEmptyMessageDelayed(MSG_SEEK, SEEK_DELAY_MS);
238             mSpeed = 1;
239             return true;
240         }
241 
242         @Override
onSetCaptionEnabled(boolean enabled)243         public void onSetCaptionEnabled(boolean enabled) {
244             // No-op
245         }
246 
247         @Override
onKeyDown(int keyCode, KeyEvent event)248         public boolean onKeyDown(int keyCode, KeyEvent event) {
249             Log.d(TAG, "onKeyDown (keyCode=" + keyCode + ", event=" + event + ")");
250             return true;
251         }
252 
253         @Override
onKeyUp(int keyCode, KeyEvent event)254         public boolean onKeyUp(int keyCode, KeyEvent event) {
255             Log.d(TAG, "onKeyUp (keyCode=" + keyCode + ", event=" + event + ")");
256             return true;
257         }
258 
259         @Override
onTimeShiftGetCurrentPosition()260         public long onTimeShiftGetCurrentPosition() {
261             Log.d(TAG, "currentPositionMs=" + mCurrentPositionMs);
262             return mCurrentPositionMs;
263         }
264 
265         @Override
onTimeShiftGetStartPosition()266         public long onTimeShiftGetStartPosition() {
267             return mRecordStartTimeMs;
268         }
269 
270         @Override
onTimeShiftPause()271         public void onTimeShiftPause() {
272             mCurrentPositionMs = mPausedTimeMs = mLastCurrentPositionUpdateTimeMs
273                     = System.currentTimeMillis();
274         }
275 
276         @Override
onTimeShiftResume()277         public void onTimeShiftResume() {
278             mSpeed = 1;
279             mPausedTimeMs = 0;
280             mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis();
281         }
282 
283         @Override
onTimeShiftSeekTo(long timeMs)284         public void onTimeShiftSeekTo(long timeMs) {
285             mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis();
286             mCurrentPositionMs = Math.max(mRecordStartTimeMs,
287                     Math.min(timeMs, mLastCurrentPositionUpdateTimeMs));
288         }
289 
290         @Override
onTimeShiftSetPlaybackParams(PlaybackParams params)291         public void onTimeShiftSetPlaybackParams(PlaybackParams params) {
292             mSpeed = params.getSpeed();
293         }
294 
295         private final class DrawRunnable implements Runnable {
296             private volatile boolean mIsCanceled = false;
297 
298             @Override
run()299             public void run() {
300                 if (mIsCanceled) {
301                     return;
302                 }
303                 if (DEBUG) {
304                     Log.v(TAG, "Draw task running");
305                 }
306                 boolean updatedState = false;
307                 ChannelState oldState;
308                 ChannelState newState = null;
309                 Surface currentSurface;
310                 ChannelInfo currentChannel;
311 
312                 synchronized (this) {
313                     oldState = mCurrentState;
314                     currentSurface = mSurface;
315                     currentChannel = mChannel;
316                     if (currentChannel != null) {
317                         newState = mBackend.getChannelState(currentChannel.originalNetworkId);
318                         if (oldState == null || newState.getVersion() > oldState.getVersion()) {
319                             mCurrentState = newState;
320                             updatedState = true;
321                         }
322                     } else {
323                         mCurrentState = null;
324                     }
325 
326                     if (currentSurface != null) {
327                         String now = new Date(mCurrentPositionMs).toString();
328                         String name = currentChannel == null ? "Null" : currentChannel.name;
329                         try {
330                             Canvas c = currentSurface.lockCanvas(null);
331                             c.drawColor(0xFF888888);
332                             c.drawText(name, 100f, 200f, mTextPaint);
333                             c.drawText(now, 100f, 400f, mTextPaint);
334                             // Assuming c.drawXXX will never fail.
335                             currentSurface.unlockCanvasAndPost(c);
336                         } catch (IllegalArgumentException e) {
337                             // The surface might have been abandoned. Ignore the exception.
338                         }
339                         if (DEBUG) {
340                             Log.v(TAG, "Post to canvas");
341                         }
342                     } else {
343                         if (DEBUG) {
344                             Log.v(TAG, "No surface");
345                         }
346                     }
347                 }
348                 if (updatedState) {
349                     update(oldState, newState, currentChannel);
350                 }
351 
352                 if (!mIsCanceled) {
353                     mHandler.postDelayed(this, REFRESH_DELAY_MS);
354                 }
355             }
356 
update(ChannelState oldState, ChannelState newState, ChannelInfo currentChannel)357             private void update(ChannelState oldState, ChannelState newState,
358                     ChannelInfo currentChannel) {
359                 Log.i(TAG, "Updating channel " + currentChannel.number + " state to " + newState);
360                 notifyTracksChanged(newState.getTrackInfoList());
361                 if (oldState == null || oldState.getTuneStatus() != newState.getTuneStatus()) {
362                     if (newState.getTuneStatus() == ChannelState.TUNE_STATUS_VIDEO_AVAILABLE) {
363                         notifyVideoAvailable();
364                         //TODO handle parental controls.
365                         notifyContentAllowed();
366                         setAudioTrack(newState.getSelectedAudioTrackId());
367                         setVideoTrack(newState.getSelectedVideoTrackId());
368                     } else {
369                         notifyVideoUnavailable(newState.getTuneStatus());
370                     }
371                 }
372             }
373 
cancel()374             public void cancel() {
375                 mIsCanceled = true;
376             }
377         }
378     }
379 
380     private class SimpleRecordingSessionImpl extends RecordingSession {
381         private final String[] PROGRAM_PROJECTION = {
382                 Programs.COLUMN_TITLE,
383                 Programs.COLUMN_EPISODE_TITLE,
384                 Programs.COLUMN_SHORT_DESCRIPTION,
385                 Programs.COLUMN_POSTER_ART_URI,
386                 Programs.COLUMN_THUMBNAIL_URI,
387                 Programs.COLUMN_CANONICAL_GENRE,
388                 Programs.COLUMN_CONTENT_RATING,
389                 Programs.COLUMN_START_TIME_UTC_MILLIS,
390                 Programs.COLUMN_END_TIME_UTC_MILLIS,
391                 Programs.COLUMN_VIDEO_WIDTH,
392                 Programs.COLUMN_VIDEO_HEIGHT,
393                 Programs.COLUMN_SEASON_DISPLAY_NUMBER,
394                 Programs.COLUMN_SEASON_TITLE,
395                 Programs.COLUMN_EPISODE_DISPLAY_NUMBER,
396         };
397 
398         private final String mInputId;
399         private long mStartTime;
400         private long mEndTime;
401         private Uri mChannelUri;
402         private Uri mProgramHintUri;
403 
SimpleRecordingSessionImpl(Context context, String inputId)404         public SimpleRecordingSessionImpl(Context context, String inputId) {
405             super(context);
406             mInputId = inputId;
407         }
408 
409         @Override
onTune(Uri uri)410         public void onTune(Uri uri) {
411             Log.i(TAG, "SimpleReccordingSesesionImpl: onTune()");
412             mTunerHelper.stopRecording(mChannelUri);
413             mChannelUri = uri;
414             ChannelInfo channel = mBackend.getChannelInfo(uri);
415             if (channel == null) {
416                 notifyError(TvInputManager.RECORDING_ERROR_UNKNOWN);
417             } else if (!mTunerHelper.tune(uri, true)) {
418                 notifyError(TvInputManager.RECORDING_ERROR_RESOURCE_BUSY);
419             } else {
420                 notifyTuned(uri);
421             }
422         }
423 
424         @Override
onStartRecording(Uri programHintUri)425         public void onStartRecording(Uri programHintUri) {
426             Log.i(TAG, "SimpleReccordingSesesionImpl: onStartRecording()");
427             mStartTime = System.currentTimeMillis();
428             mProgramHintUri = programHintUri;
429         }
430 
431         @Override
onStopRecording()432         public void onStopRecording() {
433             Log.i(TAG, "SimpleReccordingSesesionImpl: onStopRecording()");
434             mEndTime = System.currentTimeMillis();
435             final long startTime = mStartTime;
436             final long endTime = mEndTime;
437             final Uri programHintUri = mProgramHintUri;
438             final Uri channelUri = mChannelUri;
439             new AsyncTask<Void, Void, Void>() {
440                 @Override
441                 protected Void doInBackground(Void... arg0) {
442                     long time = System.currentTimeMillis();
443                     if (programHintUri != null) {
444                         // Retrieves program info from mProgramHintUri
445                         try (Cursor c = getContentResolver().query(programHintUri,
446                                 PROGRAM_PROJECTION, null, null, null)) {
447                             if (c != null && c.getCount() > 0) {
448                                 storeRecordedProgram(c, startTime, endTime);
449                                 return null;
450                             }
451                         } catch (Exception e) {
452                             Log.w(TAG, "Error querying " + this, e);
453                         }
454                     }
455                     // Retrieves the current program
456                     try (Cursor c = getContentResolver().query(
457                             TvContract.buildProgramsUriForChannel(channelUri, startTime,
458                                     endTime - startTime < MAX_COMMAND_DELAY ? startTime :
459                                             endTime - MAX_COMMAND_DELAY),
460                             PROGRAM_PROJECTION, null, null, null)) {
461                         if (c != null && c.getCount() == 1) {
462                             storeRecordedProgram(c, startTime, endTime);
463                             return null;
464                         }
465                     } catch (Exception e) {
466                         Log.w(TAG, "Error querying " + this, e);
467                     }
468                     storeRecordedProgram(null, startTime, endTime);
469                     return null;
470                 }
471 
472                 private void storeRecordedProgram(Cursor c, long startTime, long endTime) {
473                     ContentValues values = new ContentValues();
474                     values.put(RecordedPrograms.COLUMN_INPUT_ID, mInputId);
475                     values.put(RecordedPrograms.COLUMN_CHANNEL_ID,
476                             ContentUris.parseId(channelUri));
477                     values.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS,
478                             endTime - startTime);
479                     if (c != null) {
480                         int index = 0;
481                         c.moveToNext();
482                         values.put(Programs.COLUMN_TITLE, c.getString(index++));
483                         values.put(Programs.COLUMN_EPISODE_TITLE, c.getString(index++));
484                         values.put(Programs.COLUMN_SHORT_DESCRIPTION, c.getString(index++));
485                         values.put(Programs.COLUMN_POSTER_ART_URI, c.getString(index++));
486                         values.put(Programs.COLUMN_THUMBNAIL_URI, c.getString(index++));
487                         values.put(Programs.COLUMN_CANONICAL_GENRE, c.getString(index++));
488                         values.put(Programs.COLUMN_CONTENT_RATING, c.getString(index++));
489                         values.put(Programs.COLUMN_START_TIME_UTC_MILLIS, c.getLong(index++));
490                         values.put(Programs.COLUMN_END_TIME_UTC_MILLIS, c.getLong(index++));
491                         values.put(Programs.COLUMN_VIDEO_WIDTH, c.getLong(index++));
492                         values.put(Programs.COLUMN_VIDEO_HEIGHT, c.getLong(index++));
493                         values.put(Programs.COLUMN_SEASON_DISPLAY_NUMBER, c.getString(index++));
494                         values.put(Programs.COLUMN_SEASON_TITLE, c.getString(index++));
495                         values.put(Programs.COLUMN_EPISODE_DISPLAY_NUMBER,
496                                 c.getString(index++));
497                     } else {
498                         values.put(RecordedPrograms.COLUMN_TITLE, "No program info");
499                         values.put(RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, startTime);
500                         values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime);
501                     }
502                     Uri uri = getContentResolver()
503                             .insert(TvContract.RecordedPrograms.CONTENT_URI, values);
504                     notifyRecordingStopped(uri);
505                 }
506             }.execute();
507         }
508 
509         @Override
onRelease()510         public void onRelease() {
511             Log.i(TAG, "SimpleReccordingSesesionImpl: onRelease()");
512             mTunerHelper.stopRecording(mChannelUri);
513             mChannelUri = null;
514         }
515     }
516 }
517