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