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