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