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