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