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.usbtuner.tvinput;
18
19import android.annotation.TargetApi;
20import android.content.Context;
21import android.media.PlaybackParams;
22import android.media.tv.TvContentRating;
23import android.media.tv.TvInputManager;
24import android.media.tv.TvInputService;
25import android.net.Uri;
26import android.os.Build;
27import android.os.Handler;
28import android.os.Message;
29import android.os.SystemClock;
30import android.text.Html;
31import android.util.Log;
32import android.view.LayoutInflater;
33import android.view.Surface;
34import android.view.View;
35import android.view.ViewGroup;
36import android.widget.TextView;
37import android.widget.Toast;
38
39import com.google.android.exoplayer.audio.AudioCapabilities;
40import com.android.usbtuner.R;
41import com.android.usbtuner.cc.CaptionLayout;
42import com.android.usbtuner.cc.CaptionTrackRenderer;
43import com.android.usbtuner.data.Cea708Data.CaptionEvent;
44import com.android.usbtuner.data.Track.AtscCaptionTrack;
45import com.android.usbtuner.data.TunerChannel;
46import com.android.usbtuner.exoplayer.cache.CacheManager;
47import com.android.usbtuner.util.StatusTextUtils;
48import com.android.usbtuner.util.SystemPropertiesProxy;
49
50/**
51 * Provides a USB tuner TV input session. It handles Overlay UI works. Main tuner input functions
52 * are implemented in {@link TunerSessionWorker}.
53 */
54public class TunerSession extends TvInputService.Session implements Handler.Callback {
55    private static final String TAG = "TunerSession";
56    private static final boolean DEBUG = false;
57    private static final String USBTUNER_SHOW_DEBUG = "persist.usbtuner.show_debug";
58
59    public static final int MSG_UI_SHOW_MESSAGE = 1;
60    public static final int MSG_UI_HIDE_MESSAGE = 2;
61    public static final int MSG_UI_SHOW_AUDIO_UNPLAYABLE = 3;
62    public static final int MSG_UI_HIDE_AUDIO_UNPLAYABLE = 4;
63    public static final int MSG_UI_PROCESS_CAPTION_TRACK = 5;
64    public static final int MSG_UI_START_CAPTION_TRACK = 6;
65    public static final int MSG_UI_STOP_CAPTION_TRACK = 7;
66    public static final int MSG_UI_RESET_CAPTION_TRACK = 8;
67    public static final int MSG_UI_SET_STATUS_TEXT = 9;
68    public static final int MSG_UI_TOAST_RESCAN_NEEDED = 10;
69
70    private final Context mContext;
71    private final Handler mUiHandler;
72    private final View mOverlayView;
73    private final TextView mMessageView;
74    private final TextView mStatusView;
75    private final TextView mAudioStatusView;
76    private final ViewGroup mMessageLayout;
77    private final CaptionTrackRenderer mCaptionTrackRenderer;
78    private final TunerSessionWorker mSessionWorker;
79    private boolean mReleased = false;
80    private boolean mVideoAvailable = false;
81    private boolean mPlayPaused;
82    private long mTuneStartTimestamp;
83
84    public TunerSession(Context context, ChannelDataManager channelDataManager,
85            CacheManager cacheManager) {
86        super(context);
87        mContext = context;
88        mUiHandler = new Handler(this);
89        LayoutInflater inflater = (LayoutInflater)
90                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
91        mOverlayView = inflater.inflate(R.layout.ut_overlay_view, null);
92        mMessageLayout = (ViewGroup) mOverlayView.findViewById(R.id.message_layout);
93        mMessageLayout.setVisibility(View.INVISIBLE);
94        mMessageView = (TextView) mOverlayView.findViewById(R.id.message);
95        mStatusView = (TextView) mOverlayView.findViewById(R.id.tuner_status);
96        boolean showDebug = SystemPropertiesProxy.getBoolean(USBTUNER_SHOW_DEBUG, false);
97        mStatusView.setVisibility(showDebug ? View.VISIBLE : View.INVISIBLE);
98        mAudioStatusView = (TextView) mOverlayView.findViewById(R.id.audio_status);
99        mAudioStatusView.setVisibility(View.INVISIBLE);
100        mAudioStatusView.setText(Html.fromHtml(StatusTextUtils.getAudioWarningInHTML(
101                context.getString(R.string.ut_ac3_passthrough_unavailable))));
102        CaptionLayout captionLayout = (CaptionLayout) mOverlayView.findViewById(R.id.caption);
103        mCaptionTrackRenderer = new CaptionTrackRenderer(captionLayout);
104        mSessionWorker = new TunerSessionWorker(context, channelDataManager,
105                cacheManager, this);
106    }
107
108    public boolean isReleased() {
109        return mReleased;
110    }
111
112    @Override
113    public View onCreateOverlayView() {
114        return mOverlayView;
115    }
116
117    @Override
118    public boolean onSelectTrack(int type, String trackId) {
119        mSessionWorker.sendMessage(
120                TunerSessionWorker.MSG_SELECT_TRACK, type, 0, trackId);
121        return false;
122    }
123
124    @Override
125    public void onSetCaptionEnabled(boolean enabled) {
126        mSessionWorker.sendMessage(
127                TunerSessionWorker.MSG_SET_CAPTION_ENABLED, enabled);
128    }
129
130    @Override
131    public void onSetStreamVolume(float volume) {
132        mSessionWorker.sendMessage(TunerSessionWorker.MSG_SET_STREAM_VOLUME, volume);
133    }
134
135    @Override
136    public boolean onSetSurface(Surface surface) {
137        mSessionWorker.sendMessage(TunerSessionWorker.MSG_SET_SURFACE, surface);
138        return true;
139    }
140
141    @Override
142    public void onTimeShiftPause() {
143        mSessionWorker.sendMessage(TunerSessionWorker.MSG_TIMESHIFT_PAUSE);
144        mPlayPaused = true;
145    }
146
147    @Override
148    public void onTimeShiftResume() {
149        mSessionWorker.sendMessage(TunerSessionWorker.MSG_TIMESHIFT_RESUME);
150        mPlayPaused = false;
151    }
152
153    @Override
154    public void onTimeShiftSeekTo(long timeMs) {
155        if (DEBUG) Log.d(TAG, "Timeshift seekTo requested position: " + timeMs / 1000);
156        mSessionWorker.sendMessage(TunerSessionWorker.MSG_TIMESHIFT_SEEK_TO,
157                mPlayPaused ? 1 : 0, 0, timeMs);
158    }
159
160    @Override
161    public void onTimeShiftSetPlaybackParams(PlaybackParams params) {
162        mSessionWorker.sendMessage(
163                TunerSessionWorker.MSG_TIMESHIFT_SET_PLAYBACKPARAMS, params);
164    }
165
166    @Override
167    public long onTimeShiftGetStartPosition() {
168        return mSessionWorker.getStartPosition();
169    }
170
171    @Override
172    public long onTimeShiftGetCurrentPosition() {
173        return mSessionWorker.getCurrentPosition();
174    }
175
176    @Override
177    public boolean onTune(Uri channelUri) {
178        if (DEBUG) {
179            Log.d(TAG, "onTune to " + channelUri != null ? channelUri.toString() : "");
180        }
181        if (channelUri == null) {
182            Log.w(TAG, "onTune() is failed due to null channelUri.");
183            mSessionWorker.stopTune();
184            return false;
185        }
186        mTuneStartTimestamp = SystemClock.elapsedRealtime();
187        mSessionWorker.tune(channelUri);
188        mPlayPaused = false;
189        return true;
190    }
191
192    @TargetApi(Build.VERSION_CODES.N)
193    @Override
194    public void onTimeShiftPlay(Uri recordUri) {
195        if (recordUri == null) {
196            Log.w(TAG, "onTimeShiftPlay() is failed due to null channelUri.");
197            mSessionWorker.stopTune();
198            return;
199        }
200        mTuneStartTimestamp = SystemClock.elapsedRealtime();
201        mSessionWorker.tune(recordUri);
202        mPlayPaused = false;
203    }
204
205    @Override
206    public void onUnblockContent(TvContentRating unblockedRating) {
207        mSessionWorker.sendMessage(TunerSessionWorker.MSG_UNBLOCKED_RATING,
208                unblockedRating);
209    }
210
211    @Override
212    public void onRelease() {
213        if (DEBUG) {
214            Log.d(TAG, "onRelease");
215        }
216        mReleased = true;
217        mSessionWorker.release();
218        mUiHandler.removeCallbacksAndMessages(null);
219    }
220
221    /**
222     * Sets {@link AudioCapabilities}.
223     */
224    public void setAudioCapabilities(AudioCapabilities audioCapabilities) {
225        mSessionWorker.sendMessage(TunerSessionWorker.MSG_AUDIO_CAPABILITIES_CHANGED,
226                audioCapabilities);
227    }
228
229    @Override
230    public void notifyVideoAvailable() {
231        super.notifyVideoAvailable();
232        if (mTuneStartTimestamp != 0) {
233            Log.i(TAG, "[Profiler] Video available in "
234                    + (SystemClock.elapsedRealtime() - mTuneStartTimestamp) + " ms");
235            mTuneStartTimestamp = 0;
236        }
237        mVideoAvailable = true;
238    }
239
240    @Override
241    public void notifyVideoUnavailable(int reason) {
242        switch (reason) {
243            case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING:
244            case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING:
245            case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY:
246            case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
247                super.notifyVideoUnavailable(reason);
248                break;
249            case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
250            default:
251                super.notifyVideoAvailable();
252                TunerChannel channel = mSessionWorker.getCurrentChannel();
253                if (channel != null) {
254                    sendUiMessage(TunerSession.MSG_UI_SHOW_MESSAGE,
255                            mContext.getString(R.string.ut_fail_to_tune, channel.getName()));
256                } else {
257                    sendUiMessage(TunerSession.MSG_UI_SHOW_MESSAGE,
258                            mContext.getString(R.string.ut_fail_to_tune_to_unknown_channel));
259                }
260                break;
261        }
262        if (reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING
263                && reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL) {
264            notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE);
265        }
266    }
267
268    public void sendUiMessage(int message) {
269        mUiHandler.sendEmptyMessage(message);
270    }
271
272    public void sendUiMessage(int message, Object object) {
273        mUiHandler.obtainMessage(message, object).sendToTarget();
274    }
275
276    public void sendUiMessage(int message, int arg1, int arg2, Object object) {
277        mUiHandler.obtainMessage(message, arg1, arg2, object).sendToTarget();
278    }
279
280    @Override
281    public boolean handleMessage(Message msg) {
282        switch (msg.what) {
283            case MSG_UI_SHOW_MESSAGE: {
284                mMessageView.setText((String) msg.obj);
285                mMessageLayout.setVisibility(View.VISIBLE);
286                return true;
287            }
288            case MSG_UI_HIDE_MESSAGE: {
289                mMessageLayout.setVisibility(View.INVISIBLE);
290                return true;
291            }
292            case MSG_UI_SHOW_AUDIO_UNPLAYABLE: {
293                mAudioStatusView.setVisibility(View.VISIBLE);
294                return true;
295            }
296            case MSG_UI_HIDE_AUDIO_UNPLAYABLE: {
297                mAudioStatusView.setVisibility(View.INVISIBLE);
298                return true;
299            }
300            case MSG_UI_PROCESS_CAPTION_TRACK: {
301                mCaptionTrackRenderer.processCaptionEvent((CaptionEvent) msg.obj);
302                return true;
303            }
304            case MSG_UI_START_CAPTION_TRACK: {
305                mCaptionTrackRenderer.start((AtscCaptionTrack) msg.obj);
306                return true;
307            }
308            case MSG_UI_STOP_CAPTION_TRACK: {
309                mCaptionTrackRenderer.stop();
310                return true;
311            }
312            case MSG_UI_RESET_CAPTION_TRACK: {
313                mCaptionTrackRenderer.reset();
314                return true;
315            }
316            case MSG_UI_SET_STATUS_TEXT: {
317                mStatusView.setText((CharSequence) msg.obj);
318                return true;
319            }
320            case MSG_UI_TOAST_RESCAN_NEEDED: {
321                Toast.makeText(mContext, R.string.ut_rescan_needed, Toast.LENGTH_LONG).show();
322                return true;
323            }
324        }
325        return false;
326    }
327}
328