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