• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
39 import com.android.tv.common.SoftPreconditions;
40 import com.android.tv.common.ui.setup.SetupFragment;
41 import com.android.tv.tuner.ChannelScanFileParser;
42 import com.android.tv.tuner.R;
43 import com.android.tv.tuner.TunerHal;
44 import com.android.tv.tuner.TunerPreferences;
45 import com.android.tv.tuner.data.PsipData;
46 import com.android.tv.tuner.data.TunerChannel;
47 import com.android.tv.tuner.data.nano.Channel;
48 import com.android.tv.tuner.source.FileTsStreamer;
49 import com.android.tv.tuner.source.TsDataSource;
50 import com.android.tv.tuner.source.TsStreamer;
51 import com.android.tv.tuner.source.TunerTsStreamer;
52 import com.android.tv.tuner.tvinput.ChannelDataManager;
53 import com.android.tv.tuner.tvinput.EventDetector;
54 
55 import java.util.ArrayList;
56 import java.util.List;
57 import java.util.Locale;
58 import java.util.concurrent.CountDownLatch;
59 import java.util.concurrent.TimeUnit;
60 
61 /**
62  * A fragment for scanning channels.
63  */
64 public 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
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
getLayoutResourceId()140     protected int getLayoutResourceId() {
141         return R.layout.ut_channel_scan;
142     }
143 
144     @Override
getParentIdsForDelay()145     protected int[] getParentIdsForDelay() {
146         return new int[] {R.id.progress_holder};
147     }
148 
startScan(int channelMapId)149     private void startScan(int channelMapId) {
150         mChannelScanTask = new ChannelScanTask(channelMapId);
151         mChannelScanTask.execute();
152     }
153 
154     @Override
onPause()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      */
finishScan(boolean cancel)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 
ChannelAdapter()192         public ChannelAdapter() {
193             mChannels = new ArrayList<>();
194         }
195 
196         @Override
areAllItemsEnabled()197         public boolean areAllItemsEnabled() {
198             return false;
199         }
200 
201         @Override
isEnabled(int pos)202         public boolean isEnabled(int pos) {
203             return false;
204         }
205 
206         @Override
getCount()207         public int getCount() {
208             return mChannels.size();
209         }
210 
211         @Override
getItem(int pos)212         public Object getItem(int pos) {
213             return pos;
214         }
215 
216         @Override
getItemId(int pos)217         public long getItemId(int pos) {
218             return pos;
219         }
220 
221         @Override
getView(int position, View convertView, ViewGroup parent)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 
add(TunerChannel channel)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 
ChannelScanTask(int channelMapId)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 
maybeSetChannelListVisible()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 
addChannel(final TunerChannel channel)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
doInBackground(Void... params)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
onCancelled()322         protected void onCancelled() {
323             SoftPreconditions.checkState(false, TAG, "call cancelScan instead of cancel");
324         }
325 
326         @Override
onProgressUpdate(Integer... values)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 
stopScan()335         private void stopScan() {
336             if (mLatch != null) {
337                 mLatch.countDown();
338             }
339             mConditionStopped.open();
340         }
341 
cancelScan(boolean cancel)342         private void cancelScan(boolean cancel) {
343             mIsCanceled = cancel;
344             stopScan();
345         }
346 
scanChannels()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 
addChannelsWithoutVct(ChannelScanFileParser.ScanChannel scanChannel)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 
getStreamer(int type)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
onEventDetected(TunerChannel channel, List<PsipData.EitItem> items)422         public void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items) {
423             mChannelDataManager.notifyEventDetected(channel, items);
424         }
425 
426         @Override
onChannelScanDone()427         public void onChannelScanDone() {
428             if (mLatch != null) {
429                 mLatch.countDown();
430             }
431         }
432 
433         @Override
onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime)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 
showFinishingProgressDialog()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
onChannelHandlingDone()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 
FakeTsStreamer(EventDetector.EventListener eventListener)484         FakeTsStreamer(EventDetector.EventListener eventListener) {
485             mEventListener = eventListener;
486         }
487 
488         @Override
startStream(ChannelScanFileParser.ScanChannel channel)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
startStream(TunerChannel channel)510         public boolean startStream(TunerChannel channel) {
511             return false;
512         }
513 
514         @Override
stopStream()515         public void stopStream() {
516         }
517 
518         @Override
createDataSource()519         public TsDataSource createDataSource() {
520             return null;
521         }
522     }
523 }
524