Home | History | Annotate | Download | only in testinput
      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 Live TV.
     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 
     63     public static String buildInputId(Context context) {
     64         return TvContract.buildInputId(new ComponentName(context, TestTvInputService.class));
     65     }
     66 
     67     @Override
     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
     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
     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 
    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 
    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 
    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
    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
    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
    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
    207         public void onSetStreamVolume(float volume) {
    208             // No-op
    209         }
    210 
    211         @Override
    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
    247         public void onSetCaptionEnabled(boolean enabled) {
    248             // No-op
    249         }
    250 
    251         @Override
    252         public boolean onKeyDown(int keyCode, KeyEvent event) {
    253             Log.d(TAG, "onKeyDown (keyCode=" + keyCode + ", event=" + event + ")");
    254             return true;
    255         }
    256 
    257         @Override
    258         public boolean onKeyUp(int keyCode, KeyEvent event) {
    259             Log.d(TAG, "onKeyUp (keyCode=" + keyCode + ", event=" + event + ")");
    260             return true;
    261         }
    262 
    263         @Override
    264         public long onTimeShiftGetCurrentPosition() {
    265             Log.d(TAG, "currentPositionMs=" + mCurrentPositionMs);
    266             return mCurrentPositionMs;
    267         }
    268 
    269         @Override
    270         public long onTimeShiftGetStartPosition() {
    271             return mRecordStartTimeMs;
    272         }
    273 
    274         @Override
    275         public void onTimeShiftPause() {
    276             mCurrentPositionMs =
    277                     mPausedTimeMs = mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis();
    278         }
    279 
    280         @Override
    281         public void onTimeShiftResume() {
    282             mSpeed = 1;
    283             mPausedTimeMs = 0;
    284             mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis();
    285         }
    286 
    287         @Override
    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
    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
    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 
    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 
    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 
    409         public SimpleRecordingSessionImpl(Context context, String inputId) {
    410             super(context);
    411             mInputId = inputId;
    412         }
    413 
    414         @Override
    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
    430         public void onStartRecording(Uri programHintUri) {
    431             Log.i(TAG, "SimpleReccordingSesesionImpl: onStartRecording()");
    432             mStartTime = System.currentTimeMillis();
    433             mProgramHintUri = programHintUri;
    434         }
    435 
    436         @Override
    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
    528         public void onRelease() {
    529             Log.i(TAG, "SimpleReccordingSesesionImpl: onRelease()");
    530             mTunerHelper.stopRecording(mChannelUri);
    531             mChannelUri = null;
    532         }
    533     }
    534 }
    535