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.usbtuner.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.ui.setup.SetupFragment; 40 import com.android.usbtuner.ChannelScanFileParser; 41 import com.android.usbtuner.ChannelScanFileParser.ScanChannel; 42 import com.android.usbtuner.FileDataSource; 43 import com.android.usbtuner.InputStreamSource; 44 import com.android.usbtuner.R; 45 import com.android.usbtuner.UsbTunerPreferences; 46 import com.android.usbtuner.UsbTunerTsScannerSource; 47 import com.android.usbtuner.data.Channel; 48 import com.android.usbtuner.data.PsiData; 49 import com.android.usbtuner.data.PsipData; 50 import com.android.usbtuner.data.TunerChannel; 51 import com.android.usbtuner.tvinput.ChannelDataManager; 52 import com.android.usbtuner.tvinput.EventDetector; 53 54 import junit.framework.Assert; 55 56 import java.util.ArrayList; 57 import java.util.List; 58 59 /** 60 * A fragment for scanning channels. 61 */ 62 public class ScanFragment extends SetupFragment { 63 private static final String TAG = "ScanFragment"; 64 private static final boolean DEBUG = false; 65 // In the fake mode, the connection to antenna or cable is not necessary. 66 // Instead dummy channels are added. 67 private static final boolean FAKE_MODE = false; 68 69 public static final String ACTION_CATEGORY = "com.android.usbtuner.setup.ScanFragment"; 70 public static final int ACTION_CANCEL = 1; 71 public static final int ACTION_FINISH = 2; 72 73 public static final String EXTRA_FOR_CHANNEL_SCAN_FILE = "scan_file_choice"; 74 75 private static final long CHANNEL_SCAN_SHOW_DELAY_MS = 10000; 76 private static final long CHANNEL_SCAN_PERIOD_MS = 4000; 77 private static final long SHOW_PROGRESS_DIALOG_DELAY_MS = 300; 78 79 // Build channels out of the locally stored TS streams. 80 private static final boolean SCAN_LOCAL_STREAMS = true; 81 82 private ChannelDataManager mChannelDataManager; 83 private ChannelScanTask mChannelScanTask; 84 private ProgressBar mProgressBar; 85 private TextView mScanningMessage; 86 private View mChannelHolder; 87 private ChannelAdapter mAdapter; 88 private volatile boolean mChannelListVisible; 89 private Button mCancelButton; 90 91 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)92 public View onCreateView(LayoutInflater inflater, ViewGroup container, 93 Bundle savedInstanceState) { 94 View view = super.onCreateView(inflater, container, savedInstanceState); 95 mChannelDataManager = new ChannelDataManager(getActivity()); 96 mChannelDataManager.checkDataVersion(getActivity()); 97 mAdapter = new ChannelAdapter(); 98 mProgressBar = (ProgressBar) view.findViewById(R.id.tune_progress); 99 mScanningMessage = (TextView) view.findViewById(R.id.tune_description); 100 ListView channelList = (ListView) view.findViewById(R.id.channel_list); 101 channelList.setAdapter(mAdapter); 102 channelList.setOnItemClickListener(null); 103 ViewGroup progressHolder = (ViewGroup) view.findViewById(R.id.progress_holder); 104 LayoutTransition transition = new LayoutTransition(); 105 transition.enableTransitionType(LayoutTransition.CHANGING); 106 progressHolder.setLayoutTransition(transition); 107 mChannelHolder = view.findViewById(R.id.channel_holder); 108 mCancelButton = (Button) view.findViewById(R.id.tune_cancel); 109 mCancelButton.setOnClickListener(new OnClickListener() { 110 @Override 111 public void onClick(View v) { 112 finishScan(false); 113 } 114 }); 115 Bundle args = getArguments(); 116 startScan(args == null ? 0 : args.getInt(EXTRA_FOR_CHANNEL_SCAN_FILE, 0)); 117 return view; 118 } 119 120 @Override getLayoutResourceId()121 protected int getLayoutResourceId() { 122 return R.layout.ut_channel_scan; 123 } 124 125 @Override getParentIdsForDelay()126 protected int[] getParentIdsForDelay() { 127 return new int[] {R.id.progress_holder}; 128 } 129 startScan(int channelMapId)130 private void startScan(int channelMapId) { 131 mChannelScanTask = new ChannelScanTask(channelMapId); 132 mChannelScanTask.execute(); 133 } 134 135 @Override onDetach()136 public void onDetach() { 137 // Ensure scan task will stop. 138 mChannelScanTask.stopScan(); 139 super.onDetach(); 140 } 141 142 /** 143 * Finishes the current scan thread. This fragment will be popped after the scan thread ends. 144 * 145 * @param cancel a flag which indicates the scan is canceled or not. 146 */ finishScan(boolean cancel)147 public void finishScan(boolean cancel) { 148 if (mChannelScanTask != null) { 149 mChannelScanTask.cancelScan(cancel); 150 151 // Notifies a user of waiting to finish the scanning process. 152 new Handler().postDelayed(new Runnable() { 153 @Override 154 public void run() { 155 mChannelScanTask.showFinishingProgressDialog(); 156 } 157 }, SHOW_PROGRESS_DIALOG_DELAY_MS); 158 159 // Hides the cancel button. 160 mCancelButton.setEnabled(false); 161 } 162 } 163 164 private class ChannelAdapter extends BaseAdapter { 165 private final ArrayList<TunerChannel> mChannels; 166 ChannelAdapter()167 public ChannelAdapter() { 168 mChannels = new ArrayList<>(); 169 } 170 171 @Override areAllItemsEnabled()172 public boolean areAllItemsEnabled() { 173 return false; 174 } 175 176 @Override isEnabled(int pos)177 public boolean isEnabled(int pos) { 178 return false; 179 } 180 181 @Override getCount()182 public int getCount() { 183 return mChannels.size(); 184 } 185 186 @Override getItem(int pos)187 public Object getItem(int pos) { 188 return pos; 189 } 190 191 @Override getItemId(int pos)192 public long getItemId(int pos) { 193 return pos; 194 } 195 196 @Override getView(int position, View convertView, ViewGroup parent)197 public View getView(int position, View convertView, ViewGroup parent) { 198 final Context context = parent.getContext(); 199 200 if (convertView == null) { 201 LayoutInflater inflater = (LayoutInflater) context 202 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 203 convertView = inflater.inflate(R.layout.ut_channel_list, parent, false); 204 } 205 206 TextView channelNum = (TextView) convertView.findViewById(R.id.channel_num); 207 channelNum.setText(mChannels.get(position).getDisplayNumber()); 208 209 TextView channelName = (TextView) convertView.findViewById(R.id.channel_name); 210 channelName.setText(mChannels.get(position).getName()); 211 return convertView; 212 } 213 add(TunerChannel channel)214 public void add(TunerChannel channel) { 215 mChannels.add(channel); 216 notifyDataSetChanged(); 217 } 218 } 219 220 private class ChannelScanTask extends AsyncTask<Void, Integer, Void> 221 implements EventDetector.EventListener { 222 private static final int MAX_PROGRESS = 100; 223 224 private final Activity mActivity; 225 private final int mChannelMapId; 226 private final InputStreamSource mTunerSource; 227 private final InputStreamSource mFileSource; 228 private final ConditionVariable mConditionStopped; 229 230 private List<ScanChannel> mScanChannelList; 231 private boolean mIsCanceled; 232 private boolean mIsFinished; 233 private ProgressDialog mFinishingProgressDialog; 234 ChannelScanTask(int channelMapId)235 public ChannelScanTask(int channelMapId) { 236 mActivity = getActivity(); 237 mChannelMapId = channelMapId; 238 mTunerSource = FAKE_MODE ? new FakeInputStreamSource(this) 239 : new UsbTunerTsScannerSource(mActivity.getApplicationContext(), this); 240 mFileSource = SCAN_LOCAL_STREAMS ? new FileDataSource(this) : null; 241 mConditionStopped = new ConditionVariable(); 242 } 243 maybeSetChannelListVisible()244 private void maybeSetChannelListVisible() { 245 mActivity.runOnUiThread(new Runnable() { 246 @Override 247 public void run() { 248 int channelsFound = mAdapter.getCount(); 249 if (!mChannelListVisible && channelsFound > 0) { 250 String format = getResources().getQuantityString( 251 R.plurals.ut_channel_scan_message, channelsFound, channelsFound); 252 mScanningMessage.setText(String.format(format, channelsFound)); 253 mChannelHolder.setVisibility(View.VISIBLE); 254 mChannelListVisible = true; 255 } 256 } 257 }); 258 } 259 addChannel(final TunerChannel channel)260 private void addChannel(final TunerChannel channel) { 261 mActivity.runOnUiThread(new Runnable() { 262 @Override 263 public void run() { 264 mAdapter.add(channel); 265 if (mChannelListVisible) { 266 int channelsFound = mAdapter.getCount(); 267 String format = getResources().getQuantityString( 268 R.plurals.ut_channel_scan_message, channelsFound, channelsFound); 269 mScanningMessage.setText(String.format(format, channelsFound)); 270 } 271 } 272 }); 273 } 274 finishStanTask()275 private synchronized void finishStanTask() { 276 if (!mIsFinished) { 277 mIsFinished = true; 278 UsbTunerPreferences.setScannedChannelCount(mActivity.getApplicationContext(), 279 mChannelDataManager.getScannedChannelCount()); 280 // Cancel a previously shown recommendation card. 281 TunerSetupActivity.cancelRecommendationCard(mActivity.getApplicationContext()); 282 // Mark scan as done 283 UsbTunerPreferences.setScanDone(mActivity.getApplicationContext()); 284 // finishing will be done manually. 285 if (mFinishingProgressDialog != null) { 286 mFinishingProgressDialog.dismiss(); 287 } 288 mActivity.runOnUiThread(new Runnable() { 289 @Override 290 public void run() { 291 onActionClick(ACTION_CATEGORY, mIsCanceled ? ACTION_CANCEL : ACTION_FINISH); 292 } 293 }); 294 } 295 } 296 297 @Override doInBackground(Void... params)298 protected Void doInBackground(Void... params) { 299 mScanChannelList = ChannelScanFileParser.parseScanFile( 300 getResources().openRawResource(mChannelMapId)); 301 if (SCAN_LOCAL_STREAMS) { 302 FileDataSource.addLocalStreamFiles(mScanChannelList); 303 } 304 scanChannels(); 305 mChannelDataManager.setCurrentVersion(mActivity); 306 mChannelDataManager.release(); 307 finishStanTask(); 308 return null; 309 } 310 311 @Override onProgressUpdate(Integer... values)312 protected void onProgressUpdate(Integer... values) { 313 mProgressBar.setProgress(values[0]); 314 } 315 stopScan()316 private void stopScan() { 317 mConditionStopped.open(); 318 } 319 cancelScan(boolean cancel)320 private void cancelScan(boolean cancel) { 321 mIsCanceled = cancel; 322 stopScan(); 323 } 324 scanChannels()325 private void scanChannels() { 326 if (DEBUG) Log.i(TAG, "Channel scan starting"); 327 mChannelDataManager.notifyScanStarted(); 328 329 long startMs = System.currentTimeMillis(); 330 int i = 1; 331 for (ScanChannel scanChannel : mScanChannelList) { 332 int frequency = scanChannel.frequency; 333 String modulation = scanChannel.modulation; 334 Log.i(TAG, "Tuning to " + frequency + " " + modulation); 335 336 InputStreamSource source = getDataSource(scanChannel.type); 337 Assert.assertNotNull(source); 338 if (source.setScanChannel(scanChannel)) { 339 source.startStream(); 340 mConditionStopped.block(CHANNEL_SCAN_PERIOD_MS); 341 source.stopStream(); 342 343 if (System.currentTimeMillis() > startMs + CHANNEL_SCAN_SHOW_DELAY_MS 344 && !mChannelListVisible) { 345 maybeSetChannelListVisible(); 346 } 347 } 348 if (mConditionStopped.block(-1)) { 349 break; 350 } 351 onProgressUpdate(MAX_PROGRESS * i++ / mScanChannelList.size()); 352 } 353 AutoCloseableUtils.closeQuietly(mTunerSource); 354 AutoCloseableUtils.closeQuietly(mFileSource); 355 mChannelDataManager.notifyScanCompleted(); 356 if (!mConditionStopped.block(-1)) { 357 publishProgress(MAX_PROGRESS); 358 } 359 if (DEBUG) Log.i(TAG, "Channel scan ended"); 360 } 361 362 getDataSource(int type)363 private InputStreamSource getDataSource(int type) { 364 switch (type) { 365 case Channel.TYPE_TUNER: 366 return mTunerSource; 367 case Channel.TYPE_FILE: 368 return mFileSource; 369 default: 370 return null; 371 } 372 } 373 374 @Override onEventDetected(TunerChannel channel, List<PsipData.EitItem> items)375 public void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items) { 376 mChannelDataManager.notifyEventDetected(channel, items); 377 } 378 379 @Override onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime)380 public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) { 381 if (DEBUG && channelArrivedAtFirstTime) { 382 Log.d(TAG, "Found channel " + channel); 383 } 384 if (channelArrivedAtFirstTime) { 385 addChannel(channel); 386 } 387 mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime); 388 } 389 showFinishingProgressDialog()390 public synchronized void showFinishingProgressDialog() { 391 // Show a progress dialog to wait for the scanning process if it's not done yet. 392 if (!mIsFinished && mFinishingProgressDialog == null) { 393 mFinishingProgressDialog = ProgressDialog.show(mActivity, "", 394 getString(R.string.ut_setup_cancel), true, false); 395 } 396 } 397 } 398 399 private static class FakeInputStreamSource implements InputStreamSource { 400 private final EventDetector.EventListener mEventListener; 401 private int mProgramNumber = 0; 402 FakeInputStreamSource(EventDetector.EventListener eventListener)403 FakeInputStreamSource(EventDetector.EventListener eventListener) { 404 mEventListener = eventListener; 405 } 406 407 @Override getType()408 public int getType() { 409 return 0; 410 } 411 412 @Override setScanChannel(ScanChannel channel)413 public boolean setScanChannel(ScanChannel channel) { 414 return true; 415 } 416 417 @Override tuneToChannel(TunerChannel channel)418 public boolean tuneToChannel(TunerChannel channel) { 419 return false; 420 } 421 422 @Override startStream()423 public void startStream() { 424 if (++mProgramNumber % 2 == 1) { 425 return; 426 } 427 final String displayNumber = Integer.toString(mProgramNumber); 428 final String name = "Channel-" + mProgramNumber; 429 mEventListener.onChannelDetected(new TunerChannel(mProgramNumber, 430 new ArrayList<PsiData.PmtItem>()) { 431 @Override 432 public String getDisplayNumber() { 433 return displayNumber; 434 } 435 436 @Override 437 public String getName() { 438 return name; 439 } 440 }, true); 441 } 442 443 @Override stopStream()444 public void stopStream() { 445 } 446 447 @Override getLimit()448 public long getLimit() { 449 return 0; 450 } 451 452 @Override getPosition()453 public long getPosition() { 454 return 0; 455 } 456 457 @Override close()458 public void close() { 459 } 460 } 461 } 462