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