• 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                         Canvas c = currentSurface.lockCanvas(null);
330                         c.drawColor(0xFF888888);
331                         c.drawText(name, 100f, 200f, mTextPaint);
332                         c.drawText(now, 100f, 400f, mTextPaint);
333                         currentSurface.unlockCanvasAndPost(c);
334                         if (DEBUG) {
335                             Log.v(TAG, "Post to canvas");
336                         }
337                     } else {
338                         if (DEBUG) {
339                             Log.v(TAG, "No surface");
340                         }
341                     }
342                 }
343                 if (updatedState) {
344                     update(oldState, newState, currentChannel);
345                 }
346 
347                 if (!mIsCanceled) {
348                     mHandler.postDelayed(this, REFRESH_DELAY_MS);
349                 }
350             }
351 
update(ChannelState oldState, ChannelState newState, ChannelInfo currentChannel)352             private void update(ChannelState oldState, ChannelState newState,
353                     ChannelInfo currentChannel) {
354                 Log.i(TAG, "Updating channel " + currentChannel.number + " state to " + newState);
355                 notifyTracksChanged(newState.getTrackInfoList());
356                 if (oldState == null || oldState.getTuneStatus() != newState.getTuneStatus()) {
357                     if (newState.getTuneStatus() == ChannelState.TUNE_STATUS_VIDEO_AVAILABLE) {
358                         notifyVideoAvailable();
359                         //TODO handle parental controls.
360                         notifyContentAllowed();
361                         setAudioTrack(newState.getSelectedAudioTrackId());
362                         setVideoTrack(newState.getSelectedVideoTrackId());
363                     } else {
364                         notifyVideoUnavailable(newState.getTuneStatus());
365                     }
366                 }
367             }
368 
cancel()369             public void cancel() {
370                 mIsCanceled = true;
371             }
372         }
373     }
374 
375     private class SimpleRecordingSessionImpl extends RecordingSession {
376         private final String[] PROGRAM_PROJECTION = {
377                 Programs.COLUMN_TITLE,
378                 Programs.COLUMN_EPISODE_TITLE,
379                 Programs.COLUMN_SHORT_DESCRIPTION,
380                 Programs.COLUMN_POSTER_ART_URI,
381                 Programs.COLUMN_THUMBNAIL_URI,
382                 Programs.COLUMN_CANONICAL_GENRE,
383                 Programs.COLUMN_CONTENT_RATING,
384                 Programs.COLUMN_START_TIME_UTC_MILLIS,
385                 Programs.COLUMN_END_TIME_UTC_MILLIS,
386                 Programs.COLUMN_VIDEO_WIDTH,
387                 Programs.COLUMN_VIDEO_HEIGHT,
388                 Programs.COLUMN_SEASON_DISPLAY_NUMBER,
389                 Programs.COLUMN_SEASON_TITLE,
390                 Programs.COLUMN_EPISODE_DISPLAY_NUMBER,
391         };
392 
393         private final String mInputId;
394         private long mStartTime;
395         private long mEndTime;
396         private Uri mChannelUri;
397         private Uri mProgramHintUri;
398 
SimpleRecordingSessionImpl(Context context, String inputId)399         public SimpleRecordingSessionImpl(Context context, String inputId) {
400             super(context);
401             mInputId = inputId;
402         }
403 
404         @Override
onTune(Uri uri)405         public void onTune(Uri uri) {
406             Log.i(TAG, "SimpleReccordingSesesionImpl: onTune()");
407             mTunerHelper.stopRecording(mChannelUri);
408             mChannelUri = uri;
409             ChannelInfo channel = mBackend.getChannelInfo(uri);
410             if (channel == null) {
411                 notifyError(TvInputManager.RECORDING_ERROR_UNKNOWN);
412             } else if (!mTunerHelper.tune(uri, true)) {
413                 notifyError(TvInputManager.RECORDING_ERROR_RESOURCE_BUSY);
414             } else {
415                 notifyTuned(uri);
416             }
417         }
418 
419         @Override
onStartRecording(Uri programHintUri)420         public void onStartRecording(Uri programHintUri) {
421             Log.i(TAG, "SimpleReccordingSesesionImpl: onStartRecording()");
422             mStartTime = System.currentTimeMillis();
423             mProgramHintUri = programHintUri;
424         }
425 
426         @Override
onStopRecording()427         public void onStopRecording() {
428             Log.i(TAG, "SimpleReccordingSesesionImpl: onStopRecording()");
429             mEndTime = System.currentTimeMillis();
430             final long startTime = mStartTime;
431             final long endTime = mEndTime;
432             final Uri programHintUri = mProgramHintUri;
433             final Uri channelUri = mChannelUri;
434             new AsyncTask<Void, Void, Void>() {
435                 @Override
436                 protected Void doInBackground(Void... arg0) {
437                     long time = System.currentTimeMillis();
438                     if (programHintUri != null) {
439                         // Retrieves program info from mProgramHintUri
440                         try (Cursor c = getContentResolver().query(programHintUri,
441                                 PROGRAM_PROJECTION, null, null, null)) {
442                             if (c != null && c.getCount() > 0) {
443                                 storeRecordedProgram(c, startTime, endTime);
444                                 return null;
445                             }
446                         } catch (Exception e) {
447                             Log.w(TAG, "Error querying " + this, e);
448                         }
449                     }
450                     // Retrieves the current program
451                     try (Cursor c = getContentResolver().query(
452                             TvContract.buildProgramsUriForChannel(channelUri, startTime,
453                                     endTime - startTime < MAX_COMMAND_DELAY ? startTime :
454                                             endTime - MAX_COMMAND_DELAY),
455                             PROGRAM_PROJECTION, null, null, null)) {
456                         if (c != null && c.getCount() == 1) {
457                             storeRecordedProgram(c, startTime, endTime);
458                             return null;
459                         }
460                     } catch (Exception e) {
461                         Log.w(TAG, "Error querying " + this, e);
462                     }
463                     storeRecordedProgram(null, startTime, endTime);
464                     return null;
465                 }
466 
467                 private void storeRecordedProgram(Cursor c, long startTime, long endTime) {
468                     ContentValues values = new ContentValues();
469                     values.put(RecordedPrograms.COLUMN_INPUT_ID, mInputId);
470                     values.put(RecordedPrograms.COLUMN_CHANNEL_ID,
471                             ContentUris.parseId(channelUri));
472                     values.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS,
473                             endTime - startTime);
474                     if (c != null) {
475                         int index = 0;
476                         c.moveToNext();
477                         values.put(Programs.COLUMN_TITLE, c.getString(index++));
478                         values.put(Programs.COLUMN_EPISODE_TITLE, c.getString(index++));
479                         values.put(Programs.COLUMN_SHORT_DESCRIPTION, c.getString(index++));
480                         values.put(Programs.COLUMN_POSTER_ART_URI, c.getString(index++));
481                         values.put(Programs.COLUMN_THUMBNAIL_URI, c.getString(index++));
482                         values.put(Programs.COLUMN_CANONICAL_GENRE, c.getString(index++));
483                         values.put(Programs.COLUMN_CONTENT_RATING, c.getString(index++));
484                         values.put(Programs.COLUMN_START_TIME_UTC_MILLIS, c.getLong(index++));
485                         values.put(Programs.COLUMN_END_TIME_UTC_MILLIS, c.getLong(index++));
486                         values.put(Programs.COLUMN_VIDEO_WIDTH, c.getLong(index++));
487                         values.put(Programs.COLUMN_VIDEO_HEIGHT, c.getLong(index++));
488                         values.put(Programs.COLUMN_SEASON_DISPLAY_NUMBER, c.getString(index++));
489                         values.put(Programs.COLUMN_SEASON_TITLE, c.getString(index++));
490                         values.put(Programs.COLUMN_EPISODE_DISPLAY_NUMBER,
491                                 c.getString(index++));
492                     } else {
493                         values.put(RecordedPrograms.COLUMN_TITLE, "No program info");
494                         values.put(RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, startTime);
495                         values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime);
496                     }
497                     Uri uri = getContentResolver()
498                             .insert(TvContract.RecordedPrograms.CONTENT_URI, values);
499                     notifyRecordingStopped(uri);
500                 }
501             }.execute();
502         }
503 
504         @Override
onRelease()505         public void onRelease() {
506             Log.i(TAG, "SimpleReccordingSesesionImpl: onRelease()");
507             mTunerHelper.stopRecording(mChannelUri);
508             mChannelUri = null;
509         }
510     }
511 }
512