1 /*
2  * Copyright (C) 2014 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.cts.verifier.tv;
18 
19 import android.annotation.SuppressLint;
20 import android.content.BroadcastReceiver;
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.IntentFilter;
25 import android.graphics.Bitmap;
26 import android.graphics.BitmapFactory;
27 import android.graphics.Canvas;
28 import android.graphics.Color;
29 import android.graphics.Rect;
30 import android.media.PlaybackParams;
31 import android.media.tv.TvContentRating;
32 import android.media.tv.TvContract;
33 import android.media.tv.TvInputManager;
34 import android.media.tv.TvInputService;
35 import android.media.tv.TvTrackInfo;
36 import android.net.Uri;
37 import android.os.Bundle;
38 import android.os.Handler;
39 import android.os.Looper;
40 import android.os.Message;
41 import android.util.Log;
42 import android.view.LayoutInflater;
43 import android.view.Surface;
44 import android.view.View;
45 import android.widget.TextView;
46 
47 import com.android.cts.verifier.R;
48 
49 import java.util.ArrayList;
50 import java.util.List;
51 
52 @SuppressLint("NewApi")
53 public class MockTvInputService extends TvInputService {
54     private static final String TAG = "MockTvInputService";
55 
56     private static final String BROADCAST_ACTION = "action";
57     private static final String SELECT_TRACK_TYPE = "type";
58     private static final String SELECT_TRACK_ID = "id";
59     private static final String CAPTION_ENABLED = "enabled";
60     private static final String PAUSE_CALLED = "pause_called";
61     private static final float DISPLAY_RATIO_EPSILON = 0.01f;
62 
63     private static final Object sLock = new Object();
64     private static Callback sTuneCallback = null;
65     private static Callback sOverlayViewCallback = null;
66     private static Callback sBroadcastCallback = null;
67     private static Callback sUnblockContentCallback = null;
68     private static Callback sSelectTrackCallback = null;
69     private static Callback sSetCaptionEnabledCallback = null;
70     // Callbacks for time shift.
71     private static Callback sResumeAfterPauseCallback = null;
72     private static Callback sPositionTrackingCallback = null;
73     private static Callback sRewindCallback = null;
74     private static Callback sFastForwardCallback = null;
75     private static Callback sSeekToPreviousCallback = null;
76     private static Callback sSeekToNextCallback = null;
77     private static Callback sOverlayViewSizeChangedCallback = null;
78 
79     private static TvContentRating sRating = null;
80 
81     static final TvTrackInfo sEngAudioTrack =
82             new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, "audio_eng")
83             .setAudioChannelCount(2)
84             .setAudioSampleRate(48000)
85             .setLanguage("eng")
86             .build();
87     static final TvTrackInfo sSpaAudioTrack =
88             new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, "audio_spa")
89             .setAudioChannelCount(2)
90             .setAudioSampleRate(48000)
91             .setLanguage("spa")
92             .build();
93     static final TvTrackInfo sEngSubtitleTrack =
94             new TvTrackInfo.Builder(TvTrackInfo.TYPE_SUBTITLE, "subtitle_eng")
95             .setLanguage("eng")
96             .build();
97     static final TvTrackInfo sKorSubtitleTrack =
98             new TvTrackInfo.Builder(TvTrackInfo.TYPE_SUBTITLE, "subtitle_kor")
99             .setLanguage("kor")
100             .build();
101     // These parameters make the display aspect ratio of sDummyVideoTrack be 4:3,
102     // which is one of common standards.
103     static final TvTrackInfo sDummyVideoTrack =
104             new TvTrackInfo.Builder(TvTrackInfo.TYPE_VIDEO, "video_dummy")
105             .setVideoWidth(704)
106             .setVideoHeight(480)
107             .setVideoPixelAspectRatio(0.909f)
108             .setVideoFrameRate(60)
109             .build();
110 
111     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
112         @Override
113         public void onReceive(Context context, Intent intent) {
114             synchronized (sLock) {
115                 if (sBroadcastCallback != null) {
116                     String expectedAction =
117                             sBroadcastCallback.getBundle().getString(BROADCAST_ACTION);
118                     if (intent.getAction().equals(expectedAction)) {
119                         sBroadcastCallback.post();
120                         sBroadcastCallback = null;
121                     }
122                 }
123             }
124         }
125     };
126 
expectTune(View postTarget, Runnable successCallback)127     static void expectTune(View postTarget, Runnable successCallback) {
128         synchronized (sLock) {
129             sTuneCallback = new Callback(postTarget, successCallback);
130         }
131     }
132 
expectBroadcast(View postTarget, String action, Runnable successCallback)133     static void expectBroadcast(View postTarget, String action, Runnable successCallback) {
134         synchronized (sLock) {
135             sBroadcastCallback = new Callback(postTarget, successCallback);
136             sBroadcastCallback.getBundle().putString(BROADCAST_ACTION, action);
137         }
138     }
139 
expectUnblockContent(View postTarget, Runnable successCallback)140     static void expectUnblockContent(View postTarget, Runnable successCallback) {
141         synchronized (sLock) {
142             sUnblockContentCallback = new Callback(postTarget, successCallback);
143         }
144     }
145 
setBlockRating(TvContentRating rating)146     static void setBlockRating(TvContentRating rating) {
147         synchronized (sLock) {
148             sRating = rating;
149         }
150     }
151 
expectOverlayView(View postTarget, Runnable successCallback)152     static void expectOverlayView(View postTarget, Runnable successCallback) {
153         synchronized (sLock) {
154             sOverlayViewCallback = new Callback(postTarget, successCallback);
155         }
156     }
157 
expectSelectTrack(int type, String id, View postTarget, Runnable successCallback)158     static void expectSelectTrack(int type, String id, View postTarget, Runnable successCallback) {
159         synchronized (sLock) {
160             sSelectTrackCallback = new Callback(postTarget, successCallback);
161             sSelectTrackCallback.getBundle().putInt(SELECT_TRACK_TYPE, type);
162             sSelectTrackCallback.getBundle().putString(SELECT_TRACK_ID, id);
163         }
164     }
165 
expectSetCaptionEnabled(boolean enabled, View postTarget, Runnable successCallback)166     static void expectSetCaptionEnabled(boolean enabled, View postTarget,
167             Runnable successCallback) {
168         synchronized (sLock) {
169             sSetCaptionEnabledCallback = new Callback(postTarget, successCallback);
170             sSetCaptionEnabledCallback.getBundle().putBoolean(CAPTION_ENABLED, enabled);
171         }
172     }
173 
expectResumeAfterPause(View postTarget, Runnable successCallback)174     static void expectResumeAfterPause(View postTarget, Runnable successCallback) {
175         synchronized (sLock) {
176             sResumeAfterPauseCallback = new Callback(postTarget, successCallback);
177         }
178     }
179 
expectPositionTracking(View postTarget, Runnable successCallback)180     static void expectPositionTracking(View postTarget, Runnable successCallback) {
181         synchronized (sLock) {
182             sPositionTrackingCallback = new Callback(postTarget, successCallback);
183         }
184     }
185 
expectRewind(View postTarget, Runnable successCallback)186     static void expectRewind(View postTarget, Runnable successCallback) {
187         synchronized (sLock) {
188             sRewindCallback = new Callback(postTarget, successCallback);
189         }
190     }
191 
expectFastForward(View postTarget, Runnable successCallback)192     static void expectFastForward(View postTarget, Runnable successCallback) {
193         synchronized (sLock) {
194             sFastForwardCallback = new Callback(postTarget, successCallback);
195         }
196     }
197 
expectSeekToPrevious(View postTarget, Runnable successCallback)198     static void expectSeekToPrevious(View postTarget, Runnable successCallback) {
199         synchronized (sLock) {
200             sSeekToPreviousCallback = new Callback(postTarget, successCallback);
201         }
202     }
203 
expectSeekToNext(View postTarget, Runnable successCallback)204     static void expectSeekToNext(View postTarget, Runnable successCallback) {
205         synchronized (sLock) {
206             sSeekToNextCallback = new Callback(postTarget, successCallback);
207         }
208     }
209 
expectedVideoAspectRatio(View postTarget, Runnable successCallback)210     static void expectedVideoAspectRatio(View postTarget, Runnable successCallback) {
211         synchronized (sLock) {
212             sOverlayViewSizeChangedCallback = new Callback(postTarget, successCallback);
213         }
214     }
215 
getInputId(Context context)216     static String getInputId(Context context) {
217         return TvContract.buildInputId(new ComponentName(context,
218                         MockTvInputService.class.getName()));
219     }
220 
221     @Override
onCreate()222     public void onCreate() {
223         super.onCreate();
224         IntentFilter intentFilter = new IntentFilter();
225         intentFilter.addAction(TvInputManager.ACTION_BLOCKED_RATINGS_CHANGED);
226         intentFilter.addAction(TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED);
227         registerReceiver(mBroadcastReceiver, intentFilter);
228     }
229 
230     @Override
onDestroy()231     public void onDestroy() {
232         unregisterReceiver(mBroadcastReceiver);
233         super.onDestroy();
234     }
235 
236     @Override
onCreateSession(String inputId)237     public Session onCreateSession(String inputId) {
238         Session session = new MockSessionImpl(this);
239         session.setOverlayViewEnabled(true);
240         return session;
241     }
242 
243     private static class MockSessionImpl extends Session {
244         private static final int MSG_SEEK = 1000;
245         private static final int SEEK_DELAY_MS = 300;
246 
247         private final Context mContext;
248         private Surface mSurface = null;
249         private List<TvTrackInfo> mTracks = new ArrayList<>();
250 
251         private long mRecordStartTimeMs;
252         private long mPausedTimeMs;
253         // The time in milliseconds when the current position is lastly updated.
254         private long mLastCurrentPositionUpdateTimeMs;
255         // The current playback position.
256         private long mCurrentPositionMs;
257         // The current playback speed rate.
258         private float mSpeed;
259 
260         private final Handler mHandler = new Handler(Looper.getMainLooper()) {
261             @Override
262             public void handleMessage(Message msg) {
263                 if (msg.what == MSG_SEEK) {
264                     // Actually, this input doesn't play any videos, it just shows the image.
265                     // So we should simulate the playback here by changing the current playback
266                     // position periodically in order to test the time shift.
267                     // If the playback is paused, the current playback position doesn't need to be
268                     // changed.
269                     if (mPausedTimeMs == 0) {
270                         long currentTimeMs = System.currentTimeMillis();
271                         mCurrentPositionMs += (long) ((currentTimeMs
272                                 - mLastCurrentPositionUpdateTimeMs) * mSpeed);
273                         mCurrentPositionMs = Math.max(mRecordStartTimeMs,
274                                 Math.min(mCurrentPositionMs, currentTimeMs));
275                         mLastCurrentPositionUpdateTimeMs = currentTimeMs;
276                     }
277                     sendEmptyMessageDelayed(MSG_SEEK, SEEK_DELAY_MS);
278                 }
279                 super.handleMessage(msg);
280             }
281         };
282 
MockSessionImpl(Context context)283         private MockSessionImpl(Context context) {
284             super(context);
285             mContext = context;
286             mTracks.add(sEngAudioTrack);
287             mTracks.add(sSpaAudioTrack);
288             mTracks.add(sEngSubtitleTrack);
289             mTracks.add(sKorSubtitleTrack);
290             mTracks.add(sDummyVideoTrack);
291         }
292 
293         @Override
onRelease()294         public void onRelease() {
295         }
296 
draw()297         private void draw() {
298             Surface surface = mSurface;
299             if (surface == null) return;
300             if (!surface.isValid()) return;
301 
302             Canvas c = surface.lockCanvas(null);
303             if (c == null) return;
304             try {
305                 Bitmap b = BitmapFactory.decodeResource(
306                         mContext.getResources(), R.drawable.icon);
307                 int srcWidth = b.getWidth();
308                 int srcHeight = b.getHeight();
309                 int dstWidth = c.getWidth();
310                 int dstHeight = c.getHeight();
311                 c.drawColor(Color.BLACK);
312                 c.drawBitmap(b, new Rect(0, 0, srcWidth, srcHeight),
313                         new Rect(10, 10, dstWidth - 10, dstHeight - 10), null);
314             } finally {
315                 surface.unlockCanvasAndPost(c);
316             }
317         }
318 
319         @Override
onCreateOverlayView()320         public View onCreateOverlayView() {
321             LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
322                     LAYOUT_INFLATER_SERVICE);
323             View view = inflater.inflate(R.layout.tv_overlay, null);
324             TextView textView = (TextView) view.findViewById(R.id.overlay_view_text);
325             textView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
326                 @Override
327                 public void onLayoutChange(View v, int left, int top, int right, int bottom,
328                         int oldLeft, int oldTop, int oldRight, int oldBottom) {
329                     Callback overlayViewCallback = null;
330                     synchronized (sLock) {
331                         overlayViewCallback = sOverlayViewCallback;
332                         sOverlayViewCallback = null;
333                     }
334                     if (overlayViewCallback != null) {
335                         overlayViewCallback.post();
336                     }
337                 }
338             });
339             return view;
340         }
341 
342         @Override
onSetSurface(Surface surface)343         public boolean onSetSurface(Surface surface) {
344             mSurface = surface;
345             if (surface != null) {
346                 draw();
347             }
348             return true;
349         }
350 
351         @Override
onSetStreamVolume(float volume)352         public void onSetStreamVolume(float volume) {
353         }
354 
355         @Override
onTune(Uri channelUri)356         public boolean onTune(Uri channelUri) {
357             synchronized (sLock) {
358                 if (sRating != null) {
359                     notifyContentBlocked(sRating);
360                 }
361                 if (sTuneCallback != null) {
362                     sTuneCallback.post();
363                     sTuneCallback = null;
364                 }
365                 if (sRating == null) {
366                     notifyContentAllowed();
367                 }
368             }
369             notifyVideoAvailable();
370             notifyTracksChanged(mTracks);
371             notifyTrackSelected(TvTrackInfo.TYPE_AUDIO, sEngAudioTrack.getId());
372             notifyTrackSelected(TvTrackInfo.TYPE_SUBTITLE, null);
373             notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, sDummyVideoTrack.getId());
374 
375             notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_AVAILABLE);
376             mRecordStartTimeMs = mCurrentPositionMs = mLastCurrentPositionUpdateTimeMs
377                     = System.currentTimeMillis();
378             mPausedTimeMs = 0;
379             mHandler.sendEmptyMessageDelayed(MSG_SEEK, SEEK_DELAY_MS);
380             mSpeed = 1;
381             return true;
382         }
383 
384         @Override
onSelectTrack(int type, String trackId)385         public boolean onSelectTrack(int type, String trackId) {
386             synchronized (sLock) {
387                 if (sSelectTrackCallback != null) {
388                     Bundle bundle = sSelectTrackCallback.getBundle();
389                     if (bundle.getInt(SELECT_TRACK_TYPE) == type
390                             && bundle.getString(SELECT_TRACK_ID).equals(trackId)) {
391                         sSelectTrackCallback.post();
392                         sSelectTrackCallback = null;
393                     }
394                 }
395             }
396             notifyTrackSelected(type, trackId);
397             return true;
398         }
399 
400         @Override
onSetCaptionEnabled(boolean enabled)401         public void onSetCaptionEnabled(boolean enabled) {
402             synchronized (sLock) {
403                 if (sSetCaptionEnabledCallback != null) {
404                     Bundle bundle = sSetCaptionEnabledCallback.getBundle();
405                     if (bundle.getBoolean(CAPTION_ENABLED) == enabled) {
406                         sSetCaptionEnabledCallback.post();
407                         sSetCaptionEnabledCallback = null;
408                     }
409                 }
410             }
411         }
412 
413         @Override
onUnblockContent(TvContentRating unblockedRating)414         public void onUnblockContent(TvContentRating unblockedRating) {
415             synchronized (sLock) {
416                 if (sRating != null && sRating.equals(unblockedRating)) {
417                     sUnblockContentCallback.post();
418                     sRating = null;
419                     notifyContentAllowed();
420                 }
421             }
422         }
423 
424         @Override
onTimeShiftGetCurrentPosition()425         public long onTimeShiftGetCurrentPosition() {
426             synchronized (sLock) {
427                 if (sPositionTrackingCallback != null) {
428                     sPositionTrackingCallback.post();
429                     sPositionTrackingCallback = null;
430                 }
431             }
432             Log.d(TAG, "currentPositionMs=" + mCurrentPositionMs);
433             return mCurrentPositionMs;
434         }
435 
436         @Override
onTimeShiftGetStartPosition()437         public long onTimeShiftGetStartPosition() {
438             return mRecordStartTimeMs;
439         }
440 
441         @Override
onTimeShiftPause()442         public void onTimeShiftPause() {
443             synchronized (sLock) {
444                 if (sResumeAfterPauseCallback != null) {
445                     sResumeAfterPauseCallback.mBundle.putBoolean(PAUSE_CALLED, true);
446                 }
447             }
448             mCurrentPositionMs = mPausedTimeMs = mLastCurrentPositionUpdateTimeMs
449                     = System.currentTimeMillis();
450         }
451 
452         @Override
onTimeShiftResume()453         public void onTimeShiftResume() {
454             synchronized (sLock) {
455                 if (sResumeAfterPauseCallback != null
456                         && sResumeAfterPauseCallback.mBundle.getBoolean(PAUSE_CALLED)) {
457                     sResumeAfterPauseCallback.post();
458                     sResumeAfterPauseCallback = null;
459                 }
460             }
461             mSpeed = 1;
462             mPausedTimeMs = 0;
463             mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis();
464         }
465 
466         @Override
onTimeShiftSeekTo(long timeMs)467         public void onTimeShiftSeekTo(long timeMs) {
468             synchronized (sLock) {
469                 if (mCurrentPositionMs > timeMs) {
470                     if (sSeekToPreviousCallback != null) {
471                         sSeekToPreviousCallback.post();
472                         sSeekToPreviousCallback = null;
473                     }
474                 } else if (mCurrentPositionMs < timeMs) {
475                     if (sSeekToNextCallback != null) {
476                         sSeekToNextCallback.post();
477                         sSeekToNextCallback = null;
478                     }
479                 }
480             }
481             mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis();
482             mCurrentPositionMs = Math.max(mRecordStartTimeMs,
483                     Math.min(timeMs, mLastCurrentPositionUpdateTimeMs));
484         }
485 
486         @Override
onTimeShiftSetPlaybackParams(PlaybackParams params)487         public void onTimeShiftSetPlaybackParams(PlaybackParams params) {
488             synchronized(sLock) {
489                 if (params != null) {
490                     if (params.getSpeed() > 1) {
491                         if (sFastForwardCallback != null) {
492                             sFastForwardCallback.post();
493                             sFastForwardCallback = null;
494                         }
495                     } else if (params.getSpeed() < 1) {
496                         if (sRewindCallback != null) {
497                             sRewindCallback.post();
498                             sRewindCallback = null;
499                         }
500                     }
501                 }
502             }
503             mSpeed = params.getSpeed();
504         }
505 
506         @Override
onOverlayViewSizeChanged(int width, int height)507         public void onOverlayViewSizeChanged(int width, int height) {
508             synchronized(sLock) {
509                 draw();
510                 if (sOverlayViewSizeChangedCallback != null) {
511                     if (sDummyVideoTrack.getVideoHeight() <= 0
512                             || sDummyVideoTrack.getVideoWidth() <= 0) {
513                         Log.w(TAG,
514                                 "The width or height of the selected video track is invalid.");
515                     } else if (height <= 0 || width <= 0) {
516                         Log.w(TAG, "The width or height of the OverlayView is incorrect.");
517                     } else if (Math.abs((float)width / height
518                             - (float)sDummyVideoTrack.getVideoWidth()
519                             * sDummyVideoTrack.getVideoPixelAspectRatio()
520                             / sDummyVideoTrack.getVideoHeight()) < DISPLAY_RATIO_EPSILON) {
521                         // Verify the video display aspect ratio is correct
522                         // and setVideoPixelAspectRatio() works for the view size.
523                         sOverlayViewSizeChangedCallback.post();
524                         sOverlayViewSizeChangedCallback = null;
525                     }
526                 }
527             }
528         }
529     }
530 
531     private static class Callback {
532         private final View mPostTarget;
533         private final Runnable mAction;
534         private final Bundle mBundle = new Bundle();
535 
Callback(View postTarget, Runnable action)536         Callback(View postTarget, Runnable action) {
537             mPostTarget = postTarget;
538             mAction = action;
539         }
540 
post()541         public void post() {
542             mPostTarget.post(mAction);
543         }
544 
getBundle()545         public Bundle getBundle() {
546             return mBundle;
547         }
548     }
549 }
550