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