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 
buildInputId(Context context)54     public static String buildInputId(Context context) {
55         return TvContract.buildInputId(new ComponentName(context, TestTvInputService.class));
56     }
57 
58     @Override
onCreate()59     public void onCreate() {
60         super.onCreate();
61         mBackend.init(this, buildInputId(this));
62     }
63 
64     @Override
onCreateSession(String inputId)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 
SimpleSessionImpl(Context context)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 
setAudioTrack(String selectedAudioTrackId)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 
setVideoTrack(String selectedVideoTrackId)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
onRelease()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
onSetSurface(Surface surface)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
onSurfaceChanged(int format, int width, int height)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
onSetStreamVolume(float volume)176         public void onSetStreamVolume(float volume) {
177             // No-op
178         }
179 
180         @Override
onTune(Uri channelUri)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
onSetCaptionEnabled(boolean enabled)211         public void onSetCaptionEnabled(boolean enabled) {
212             // No-op
213         }
214 
215         @Override
onKeyDown(int keyCode, KeyEvent event)216         public boolean onKeyDown(int keyCode, KeyEvent event) {
217             Log.d(TAG, "onKeyDown (keyCode=" + keyCode + ", event=" + event + ")");
218             return true;
219         }
220 
221         @Override
onKeyUp(int keyCode, KeyEvent event)222         public boolean onKeyUp(int keyCode, KeyEvent event) {
223             Log.d(TAG, "onKeyUp (keyCode=" + keyCode + ", event=" + event + ")");
224             return true;
225         }
226 
227         @Override
onTimeShiftGetCurrentPosition()228         public long onTimeShiftGetCurrentPosition() {
229             Log.d(TAG, "currentPositionMs=" + mCurrentPositionMs);
230             return mCurrentPositionMs;
231         }
232 
233         @Override
onTimeShiftGetStartPosition()234         public long onTimeShiftGetStartPosition() {
235             return mRecordStartTimeMs;
236         }
237 
238         @Override
onTimeShiftPause()239         public void onTimeShiftPause() {
240             mCurrentPositionMs = mPausedTimeMs = mLastCurrentPositionUpdateTimeMs
241                     = System.currentTimeMillis();
242         }
243 
244         @Override
onTimeShiftResume()245         public void onTimeShiftResume() {
246             mSpeed = 1;
247             mPausedTimeMs = 0;
248             mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis();
249         }
250 
251         @Override
onTimeShiftSeekTo(long timeMs)252         public void onTimeShiftSeekTo(long timeMs) {
253             mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis();
254             mCurrentPositionMs = Math.max(mRecordStartTimeMs,
255                     Math.min(timeMs, mLastCurrentPositionUpdateTimeMs));
256         }
257 
258         @Override
onTimeShiftSetPlaybackParams(PlaybackParams params)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
run()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 
update(ChannelState oldState, ChannelState newState, ChannelInfo currentChannel)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 
draw(Surface surface, ChannelInfo currentChannel)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 
cancel()342             public void cancel() {
343                 mIsCanceled = true;
344             }
345         }
346     }
347 }