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.tuner.setup;
18
19import android.animation.LayoutTransition;
20import android.app.Activity;
21import android.app.ProgressDialog;
22import android.content.Context;
23import android.os.AsyncTask;
24import android.os.Build;
25import android.os.Bundle;
26import android.os.ConditionVariable;
27import android.os.Handler;
28import android.util.Log;
29import android.view.LayoutInflater;
30import android.view.View;
31import android.view.View.OnClickListener;
32import android.view.ViewGroup;
33import android.widget.BaseAdapter;
34import android.widget.Button;
35import android.widget.ListView;
36import android.widget.ProgressBar;
37import android.widget.TextView;
38
39import com.android.tv.common.SoftPreconditions;
40import com.android.tv.common.ui.setup.SetupFragment;
41import com.android.tv.tuner.ChannelScanFileParser;
42import com.android.tv.tuner.R;
43import com.android.tv.tuner.TunerHal;
44import com.android.tv.tuner.TunerPreferences;
45import com.android.tv.tuner.data.PsipData;
46import com.android.tv.tuner.data.TunerChannel;
47import com.android.tv.tuner.data.nano.Channel;
48import com.android.tv.tuner.source.FileTsStreamer;
49import com.android.tv.tuner.source.TsDataSource;
50import com.android.tv.tuner.source.TsStreamer;
51import com.android.tv.tuner.source.TunerTsStreamer;
52import com.android.tv.tuner.tvinput.ChannelDataManager;
53import com.android.tv.tuner.tvinput.EventDetector;
54
55import java.util.ArrayList;
56import java.util.List;
57import java.util.Locale;
58import java.util.concurrent.CountDownLatch;
59import java.util.concurrent.TimeUnit;
60
61/**
62 * A fragment for scanning channels.
63 */
64public class ScanFragment extends SetupFragment {
65    private static final String TAG = "ScanFragment";
66    private static final boolean DEBUG = false;
67
68    // In the fake mode, the connection to antenna or cable is not necessary.
69    // Instead dummy channels are added.
70    private static final boolean FAKE_MODE = false;
71
72    private static final String VCTLESS_CHANNEL_NAME_FORMAT = "RF%d-%d";
73
74    public static final String ACTION_CATEGORY = "com.android.tv.tuner.setup.ScanFragment";
75    public static final int ACTION_CANCEL = 1;
76    public static final int ACTION_FINISH = 2;
77
78    public static final String EXTRA_FOR_CHANNEL_SCAN_FILE = "scan_file_choice";
79
80    private static final long CHANNEL_SCAN_SHOW_DELAY_MS = 10000;
81    private static final long CHANNEL_SCAN_PERIOD_MS = 4000;
82    private static final long SHOW_PROGRESS_DIALOG_DELAY_MS = 300;
83
84    // Build channels out of the locally stored TS streams.
85    private static final boolean SCAN_LOCAL_STREAMS = true;
86
87    private ChannelDataManager mChannelDataManager;
88    private ChannelScanTask mChannelScanTask;
89    private ProgressBar mProgressBar;
90    private TextView mScanningMessage;
91    private View mChannelHolder;
92    private ChannelAdapter mAdapter;
93    private volatile boolean mChannelListVisible;
94    private Button mCancelButton;
95
96    @Override
97    public View onCreateView(LayoutInflater inflater, ViewGroup container,
98            Bundle savedInstanceState) {
99        if (DEBUG) Log.d(TAG, "onCreateView");
100        View view = super.onCreateView(inflater, container, savedInstanceState);
101        mChannelDataManager = new ChannelDataManager(getActivity());
102        mChannelDataManager.checkDataVersion(getActivity());
103        mAdapter = new ChannelAdapter();
104        mProgressBar = (ProgressBar) view.findViewById(R.id.tune_progress);
105        mScanningMessage = (TextView) view.findViewById(R.id.tune_description);
106        ListView channelList = (ListView) view.findViewById(R.id.channel_list);
107        channelList.setAdapter(mAdapter);
108        channelList.setOnItemClickListener(null);
109        ViewGroup progressHolder = (ViewGroup) view.findViewById(R.id.progress_holder);
110        LayoutTransition transition = new LayoutTransition();
111        transition.enableTransitionType(LayoutTransition.CHANGING);
112        progressHolder.setLayoutTransition(transition);
113        mChannelHolder = view.findViewById(R.id.channel_holder);
114        mCancelButton = (Button) view.findViewById(R.id.tune_cancel);
115        mCancelButton.setOnClickListener(new OnClickListener() {
116            @Override
117            public void onClick(View v) {
118                finishScan(false);
119            }
120        });
121        Bundle args = getArguments();
122        int tunerType = (args == null ? 0 : args.getInt(TunerSetupActivity.KEY_TUNER_TYPE, 0));
123        // TODO: Handle the case when the fragment is restored.
124        startScan(args == null ? 0 : args.getInt(EXTRA_FOR_CHANNEL_SCAN_FILE, 0));
125        TextView scanTitleView = (TextView) view.findViewById(R.id.tune_title);
126        switch (tunerType) {
127            case TunerHal.TUNER_TYPE_USB:
128                scanTitleView.setText(R.string.ut_channel_scan);
129                break;
130            case TunerHal.TUNER_TYPE_NETWORK:
131                scanTitleView.setText(R.string.nt_channel_scan);
132                break;
133            default:
134                scanTitleView.setText(R.string.bt_channel_scan);
135        }
136        return view;
137    }
138
139    @Override
140    protected int getLayoutResourceId() {
141        return R.layout.ut_channel_scan;
142    }
143
144    @Override
145    protected int[] getParentIdsForDelay() {
146        return new int[] {R.id.progress_holder};
147    }
148
149    private void startScan(int channelMapId) {
150        mChannelScanTask = new ChannelScanTask(channelMapId);
151        mChannelScanTask.execute();
152    }
153
154    @Override
155    public void onPause() {
156        Log.d(TAG, "onPause");
157        if (mChannelScanTask != null) {
158            // Ensure scan task will stop.
159            Log.w(TAG, "The activity went to the background. Stopping channel scan.");
160            mChannelScanTask.stopScan();
161        }
162        super.onPause();
163    }
164
165    /**
166     * Finishes the current scan thread. This fragment will be popped after the scan thread ends.
167     *
168     * @param cancel a flag which indicates the scan is canceled or not.
169     */
170    public void finishScan(boolean cancel) {
171        if (mChannelScanTask != null) {
172            mChannelScanTask.cancelScan(cancel);
173
174            // Notifies a user of waiting to finish the scanning process.
175            new Handler().postDelayed(new Runnable() {
176                @Override
177                public void run() {
178                    if (mChannelScanTask != null) {
179                        mChannelScanTask.showFinishingProgressDialog();
180                    }
181                }
182            }, SHOW_PROGRESS_DIALOG_DELAY_MS);
183
184            // Hides the cancel button.
185            mCancelButton.setEnabled(false);
186        }
187    }
188
189    private class ChannelAdapter extends BaseAdapter {
190        private final ArrayList<TunerChannel> mChannels;
191
192        public ChannelAdapter() {
193            mChannels = new ArrayList<>();
194        }
195
196        @Override
197        public boolean areAllItemsEnabled() {
198            return false;
199        }
200
201        @Override
202        public boolean isEnabled(int pos) {
203            return false;
204        }
205
206        @Override
207        public int getCount() {
208            return mChannels.size();
209        }
210
211        @Override
212        public Object getItem(int pos) {
213            return pos;
214        }
215
216        @Override
217        public long getItemId(int pos) {
218            return pos;
219        }
220
221        @Override
222        public View getView(int position, View convertView, ViewGroup parent) {
223            final Context context = parent.getContext();
224
225            if (convertView == null) {
226                LayoutInflater inflater = (LayoutInflater) context
227                        .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
228                convertView = inflater.inflate(R.layout.ut_channel_list, parent, false);
229            }
230
231            TextView channelNum = (TextView) convertView.findViewById(R.id.channel_num);
232            channelNum.setText(mChannels.get(position).getDisplayNumber());
233
234            TextView channelName = (TextView) convertView.findViewById(R.id.channel_name);
235            channelName.setText(mChannels.get(position).getName());
236            return convertView;
237        }
238
239        public void add(TunerChannel channel) {
240            mChannels.add(channel);
241            notifyDataSetChanged();
242        }
243    }
244
245    private class ChannelScanTask extends AsyncTask<Void, Integer, Void>
246            implements EventDetector.EventListener, ChannelDataManager.ChannelScanListener {
247        private static final int MAX_PROGRESS = 100;
248
249        private final Activity mActivity;
250        private final int mChannelMapId;
251        private final TsStreamer mScanTsStreamer;
252        private final TsStreamer mFileTsStreamer;
253        private final ConditionVariable mConditionStopped;
254
255        private final List<ChannelScanFileParser.ScanChannel> mScanChannelList = new ArrayList<>();
256        private boolean mIsCanceled;
257        private boolean mIsFinished;
258        private ProgressDialog mFinishingProgressDialog;
259        private CountDownLatch mLatch;
260
261        public ChannelScanTask(int channelMapId) {
262            mActivity = getActivity();
263            mChannelMapId = channelMapId;
264            if (FAKE_MODE) {
265                mScanTsStreamer = new FakeTsStreamer(this);
266            } else {
267                TunerHal hal = ((TunerSetupActivity) mActivity).getTunerHal();
268                if (hal == null) {
269                    throw new RuntimeException("Failed to open a DVB device");
270                }
271                mScanTsStreamer = new TunerTsStreamer(hal, this);
272            }
273            mFileTsStreamer = SCAN_LOCAL_STREAMS ? new FileTsStreamer(this, mActivity) : null;
274            mConditionStopped = new ConditionVariable();
275            mChannelDataManager.setChannelScanListener(this, new Handler());
276        }
277
278        private void maybeSetChannelListVisible() {
279            mActivity.runOnUiThread(new Runnable() {
280                @Override
281                public void run() {
282                    int channelsFound = mAdapter.getCount();
283                    if (!mChannelListVisible && channelsFound > 0) {
284                        String format = getResources().getQuantityString(
285                                R.plurals.ut_channel_scan_message, channelsFound, channelsFound);
286                        mScanningMessage.setText(String.format(format, channelsFound));
287                        mChannelHolder.setVisibility(View.VISIBLE);
288                        mChannelListVisible = true;
289                    }
290                }
291            });
292        }
293
294        private void addChannel(final TunerChannel channel) {
295            mActivity.runOnUiThread(new Runnable() {
296                @Override
297                public void run() {
298                    mAdapter.add(channel);
299                    if (mChannelListVisible) {
300                        int channelsFound = mAdapter.getCount();
301                        String format = getResources().getQuantityString(
302                                R.plurals.ut_channel_scan_message, channelsFound, channelsFound);
303                        mScanningMessage.setText(String.format(format, channelsFound));
304                    }
305                }
306            });
307        }
308
309        @Override
310        protected Void doInBackground(Void... params) {
311            mScanChannelList.clear();
312            if (SCAN_LOCAL_STREAMS) {
313                FileTsStreamer.addLocalStreamFiles(mScanChannelList);
314            }
315            mScanChannelList.addAll(ChannelScanFileParser.parseScanFile(
316                    getResources().openRawResource(mChannelMapId)));
317            scanChannels();
318            return null;
319        }
320
321        @Override
322        protected void onCancelled() {
323            SoftPreconditions.checkState(false, TAG, "call cancelScan instead of cancel");
324        }
325
326        @Override
327        protected void onProgressUpdate(Integer... values) {
328            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
329                mProgressBar.setProgress(values[0], true);
330            } else {
331                mProgressBar.setProgress(values[0]);
332            }
333        }
334
335        private void stopScan() {
336            if (mLatch != null) {
337                mLatch.countDown();
338            }
339            mConditionStopped.open();
340        }
341
342        private void cancelScan(boolean cancel) {
343            mIsCanceled = cancel;
344            stopScan();
345        }
346
347        private void scanChannels() {
348            if (DEBUG) Log.i(TAG, "Channel scan starting");
349            mChannelDataManager.notifyScanStarted();
350
351            long startMs = System.currentTimeMillis();
352            int i = 1;
353            for (ChannelScanFileParser.ScanChannel scanChannel : mScanChannelList) {
354                int frequency = scanChannel.frequency;
355                String modulation = scanChannel.modulation;
356                Log.i(TAG, "Tuning to " + frequency + " " + modulation);
357
358                TsStreamer streamer = getStreamer(scanChannel.type);
359                SoftPreconditions.checkNotNull(streamer);
360                if (streamer != null && streamer.startStream(scanChannel)) {
361                    mLatch = new CountDownLatch(1);
362                    try {
363                        mLatch.await(CHANNEL_SCAN_PERIOD_MS, TimeUnit.MILLISECONDS);
364                    } catch (InterruptedException e) {
365                        Log.e(TAG, "The current thread is interrupted during scanChannels(). " +
366                                "The TS stream is stopped earlier than expected.", e);
367                    }
368                    streamer.stopStream();
369
370                    addChannelsWithoutVct(scanChannel);
371                    if (System.currentTimeMillis() > startMs + CHANNEL_SCAN_SHOW_DELAY_MS
372                            && !mChannelListVisible) {
373                        maybeSetChannelListVisible();
374                    }
375                }
376                if (mConditionStopped.block(-1)) {
377                    break;
378                }
379                publishProgress(MAX_PROGRESS * i++ / mScanChannelList.size());
380            }
381            mChannelDataManager.notifyScanCompleted();
382            if (!mConditionStopped.block(-1)) {
383                publishProgress(MAX_PROGRESS);
384            }
385            if (DEBUG) Log.i(TAG, "Channel scan ended");
386        }
387
388
389        private void addChannelsWithoutVct(ChannelScanFileParser.ScanChannel scanChannel) {
390            if (scanChannel.radioFrequencyNumber == null
391                    || !(mScanTsStreamer instanceof TunerTsStreamer)) {
392                return;
393            }
394            for (TunerChannel tunerChannel
395                    : ((TunerTsStreamer) mScanTsStreamer).getMalFormedChannels()) {
396                if ((tunerChannel.getVideoPid() != TunerChannel.INVALID_PID)
397                        && (tunerChannel.getAudioPid() != TunerChannel.INVALID_PID)) {
398                    tunerChannel.setFrequency(scanChannel.frequency);
399                    tunerChannel.setModulation(scanChannel.modulation);
400                    tunerChannel.setShortName(String.format(Locale.US, VCTLESS_CHANNEL_NAME_FORMAT,
401                            scanChannel.radioFrequencyNumber,
402                            tunerChannel.getProgramNumber()));
403                    tunerChannel.setVirtualMajor(scanChannel.radioFrequencyNumber);
404                    tunerChannel.setVirtualMinor(tunerChannel.getProgramNumber());
405                    onChannelDetected(tunerChannel, true);
406                }
407            }
408        }
409
410        private TsStreamer getStreamer(int type) {
411            switch (type) {
412                case Channel.TYPE_TUNER:
413                    return mScanTsStreamer;
414                case Channel.TYPE_FILE:
415                    return mFileTsStreamer;
416                default:
417                    return null;
418            }
419        }
420
421        @Override
422        public void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items) {
423            mChannelDataManager.notifyEventDetected(channel, items);
424        }
425
426        @Override
427        public void onChannelScanDone() {
428            if (mLatch != null) {
429                mLatch.countDown();
430            }
431        }
432
433        @Override
434        public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) {
435            if (channelArrivedAtFirstTime) {
436                Log.i(TAG, "Found channel " + channel);
437            }
438            if (channelArrivedAtFirstTime && channel.hasAudio()) {
439                // Playbacks with video-only stream have not been tested yet.
440                // No video-only channel has been found.
441                addChannel(channel);
442                mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime);
443            }
444        }
445
446        public void showFinishingProgressDialog() {
447            // Show a progress dialog to wait for the scanning process if it's not done yet.
448            if (!mIsFinished && mFinishingProgressDialog == null) {
449                mFinishingProgressDialog = ProgressDialog.show(mActivity, "",
450                        getString(R.string.ut_setup_cancel), true, false);
451            }
452        }
453
454        @Override
455        public void onChannelHandlingDone() {
456            mChannelDataManager.setCurrentVersion(mActivity);
457            mChannelDataManager.releaseSafely();
458            mIsFinished = true;
459            TunerPreferences.setScannedChannelCount(mActivity.getApplicationContext(),
460                    mChannelDataManager.getScannedChannelCount());
461            // Cancel a previously shown notification.
462            TunerSetupActivity.cancelNotification(mActivity.getApplicationContext());
463            // Mark scan as done
464            TunerPreferences.setScanDone(mActivity.getApplicationContext());
465            // finishing will be done manually.
466            if (mFinishingProgressDialog != null) {
467                mFinishingProgressDialog.dismiss();
468            }
469            // If the fragment is not resumed, the next fragment (scan result page) can't be
470            // displayed. In that case, just close the activity.
471            if (isResumed()) {
472                onActionClick(ACTION_CATEGORY, mIsCanceled ? ACTION_CANCEL : ACTION_FINISH);
473            } else if (getActivity() != null) {
474                getActivity().finish();
475            }
476            mChannelScanTask = null;
477        }
478    }
479
480    private static class FakeTsStreamer implements TsStreamer {
481        private final EventDetector.EventListener mEventListener;
482        private int mProgramNumber = 0;
483
484        FakeTsStreamer(EventDetector.EventListener eventListener) {
485            mEventListener = eventListener;
486        }
487
488        @Override
489        public boolean startStream(ChannelScanFileParser.ScanChannel channel) {
490            if (++mProgramNumber % 2 == 1) {
491                return true;
492            }
493            final String displayNumber = Integer.toString(mProgramNumber);
494            final String name = "Channel-" + mProgramNumber;
495            mEventListener.onChannelDetected(new TunerChannel(mProgramNumber, new ArrayList<>()) {
496                @Override
497                public String getDisplayNumber() {
498                    return displayNumber;
499                }
500
501                @Override
502                public String getName() {
503                    return name;
504                }
505            }, true);
506            return true;
507        }
508
509        @Override
510        public boolean startStream(TunerChannel channel) {
511            return false;
512        }
513
514        @Override
515        public void stopStream() {
516        }
517
518        @Override
519        public TsDataSource createDataSource() {
520            return null;
521        }
522    }
523}
524