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
17package com.android.tv.testinput;
18
19import android.content.ComponentName;
20import android.content.Context;
21import android.graphics.Canvas;
22import android.graphics.Color;
23import android.graphics.Paint;
24import android.media.PlaybackParams;
25import android.media.tv.TvContract;
26import android.media.tv.TvInputManager;
27import android.media.tv.TvInputService;
28import android.media.tv.TvTrackInfo;
29import android.net.Uri;
30import android.os.Build;
31import android.os.Handler;
32import android.os.Looper;
33import android.os.Message;
34import android.util.Log;
35import android.view.KeyEvent;
36import android.view.Surface;
37
38import com.android.tv.testing.ChannelInfo;
39import com.android.tv.testing.testinput.ChannelState;
40
41import java.util.Date;
42
43/**
44 * Simple TV input service which provides test channels.
45 */
46public 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}