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