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