diff options
Diffstat (limited to 'tuner/src/com/android/tv/tuner/setup/ScanFragment.java')
-rw-r--r-- | tuner/src/com/android/tv/tuner/setup/ScanFragment.java | 553 |
1 files changed, 553 insertions, 0 deletions
diff --git a/tuner/src/com/android/tv/tuner/setup/ScanFragment.java b/tuner/src/com/android/tv/tuner/setup/ScanFragment.java new file mode 100644 index 00000000..3ac86e19 --- /dev/null +++ b/tuner/src/com/android/tv/tuner/setup/ScanFragment.java @@ -0,0 +1,553 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.tuner.setup; + +import android.animation.LayoutTransition; +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Context; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.os.ConditionVariable; +import android.os.Handler; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Button; +import android.widget.ListView; +import android.widget.ProgressBar; +import android.widget.TextView; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.ui.setup.SetupFragment; +import com.android.tv.tuner.ChannelScanFileParser; +import com.android.tv.tuner.R; +import com.android.tv.tuner.TunerHal; +import com.android.tv.tuner.TunerPreferences; +import com.android.tv.tuner.data.PsipData; +import com.android.tv.tuner.data.TunerChannel; +import com.android.tv.tuner.data.nano.Channel; + + +import com.android.tv.tuner.source.FileTsStreamer; +import com.android.tv.tuner.source.TsDataSource; +import com.android.tv.tuner.source.TsStreamer; +import com.android.tv.tuner.source.TunerTsStreamer; +import com.android.tv.tuner.tvinput.ChannelDataManager; +import com.android.tv.tuner.tvinput.EventDetector; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** A fragment for scanning channels. */ +public class ScanFragment extends SetupFragment { + private static final String TAG = "ScanFragment"; + private static final boolean DEBUG = false; + + // In the fake mode, the connection to antenna or cable is not necessary. + // Instead dummy channels are added. + private static final boolean FAKE_MODE = false; + + private static final String VCTLESS_CHANNEL_NAME_FORMAT = "RF%d-%d"; + + public static final String ACTION_CATEGORY = "com.android.tv.tuner.setup.ScanFragment"; + public static final int ACTION_CANCEL = 1; + public static final int ACTION_FINISH = 2; + + public static final String EXTRA_FOR_CHANNEL_SCAN_FILE = "scan_file_choice"; + public static final String KEY_CHANNEL_NUMBERS = "channel_numbers"; + private static final long CHANNEL_SCAN_SHOW_DELAY_MS = 10000; + private static final long CHANNEL_SCAN_PERIOD_MS = 4000; + private static final long SHOW_PROGRESS_DIALOG_DELAY_MS = 300; + + // Build channels out of the locally stored TS streams. + private static final boolean SCAN_LOCAL_STREAMS = true; + + private ChannelDataManager mChannelDataManager; + private ChannelScanTask mChannelScanTask; + private ProgressBar mProgressBar; + private TextView mScanningMessage; + private View mChannelHolder; + private ChannelAdapter mAdapter; + private volatile boolean mChannelListVisible; + private Button mCancelButton; + + private ArrayList<String> mChannelNumbers; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + if (DEBUG) Log.d(TAG, "onCreateView"); + View view = super.onCreateView(inflater, container, savedInstanceState); + mChannelNumbers = new ArrayList<>(); + mChannelDataManager = new ChannelDataManager(getActivity()); + mChannelDataManager.checkDataVersion(getActivity()); + mAdapter = new ChannelAdapter(); + mProgressBar = (ProgressBar) view.findViewById(R.id.tune_progress); + mScanningMessage = (TextView) view.findViewById(R.id.tune_description); + ListView channelList = (ListView) view.findViewById(R.id.channel_list); + channelList.setAdapter(mAdapter); + channelList.setOnItemClickListener(null); + ViewGroup progressHolder = (ViewGroup) view.findViewById(R.id.progress_holder); + LayoutTransition transition = new LayoutTransition(); + transition.enableTransitionType(LayoutTransition.CHANGING); + progressHolder.setLayoutTransition(transition); + mChannelHolder = view.findViewById(R.id.channel_holder); + mCancelButton = (Button) view.findViewById(R.id.tune_cancel); + mCancelButton.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + finishScan(false); + } + }); + Bundle args = getArguments(); + int tunerType = (args == null ? 0 : args.getInt(BaseTunerSetupActivity.KEY_TUNER_TYPE, 0)); + // TODO: Handle the case when the fragment is restored. + startScan(args == null ? 0 : args.getInt(EXTRA_FOR_CHANNEL_SCAN_FILE, 0)); + TextView scanTitleView = (TextView) view.findViewById(R.id.tune_title); + switch (tunerType) { + case TunerHal.TUNER_TYPE_USB: + scanTitleView.setText(R.string.ut_channel_scan); + break; + case TunerHal.TUNER_TYPE_NETWORK: + scanTitleView.setText(R.string.nt_channel_scan); + break; + default: + scanTitleView.setText(R.string.bt_channel_scan); + } + return view; + } + + @Override + protected int getLayoutResourceId() { + return R.layout.ut_channel_scan; + } + + @Override + protected int[] getParentIdsForDelay() { + return new int[] {R.id.progress_holder}; + } + + private void startScan(int channelMapId) { + mChannelScanTask = new ChannelScanTask(channelMapId); + mChannelScanTask.execute(); + } + + @Override + public void onPause() { + Log.d(TAG, "onPause"); + if (mChannelScanTask != null) { + // Ensure scan task will stop. + Log.w(TAG, "The activity went to the background. Stopping channel scan."); + mChannelScanTask.stopScan(); + } + super.onPause(); + } + + /** + * Finishes the current scan thread. This fragment will be popped after the scan thread ends. + * + * @param cancel a flag which indicates the scan is canceled or not. + */ + public void finishScan(boolean cancel) { + if (mChannelScanTask != null) { + mChannelScanTask.cancelScan(cancel); + + // Notifies a user of waiting to finish the scanning process. + new Handler() + .postDelayed( + new Runnable() { + @Override + public void run() { + if (mChannelScanTask != null) { + mChannelScanTask.showFinishingProgressDialog(); + } + } + }, + SHOW_PROGRESS_DIALOG_DELAY_MS); + + // Hides the cancel button. + mCancelButton.setEnabled(false); + } + } + + private static class ChannelAdapter extends BaseAdapter { + private final ArrayList<TunerChannel> mChannels; + + public ChannelAdapter() { + mChannels = new ArrayList<>(); + } + + @Override + public boolean areAllItemsEnabled() { + return false; + } + + @Override + public boolean isEnabled(int pos) { + return false; + } + + @Override + public int getCount() { + return mChannels.size(); + } + + @Override + public Object getItem(int pos) { + return pos; + } + + @Override + public long getItemId(int pos) { + return pos; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final Context context = parent.getContext(); + + if (convertView == null) { + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + convertView = inflater.inflate(R.layout.ut_channel_list, parent, false); + } + + TextView channelNum = (TextView) convertView.findViewById(R.id.channel_num); + channelNum.setText(mChannels.get(position).getDisplayNumber()); + + TextView channelName = (TextView) convertView.findViewById(R.id.channel_name); + channelName.setText(mChannels.get(position).getName()); + return convertView; + } + + public void add(TunerChannel channel) { + mChannels.add(channel); + notifyDataSetChanged(); + } + } + + private class ChannelScanTask extends AsyncTask<Void, Integer, Void> + implements EventDetector.EventListener, ChannelDataManager.ChannelScanListener { + private static final int MAX_PROGRESS = 100; + + private final Activity mActivity; + private final int mChannelMapId; + private final TsStreamer mScanTsStreamer; + private final TsStreamer mFileTsStreamer; + private final ConditionVariable mConditionStopped; + + private final List<ChannelScanFileParser.ScanChannel> mScanChannelList = new ArrayList<>(); + private boolean mIsCanceled; + private boolean mIsFinished; + private ProgressDialog mFinishingProgressDialog; + private CountDownLatch mLatch; + + public ChannelScanTask(int channelMapId) { + mActivity = getActivity(); + mChannelMapId = channelMapId; + if (FAKE_MODE) { + mScanTsStreamer = new FakeTsStreamer(this); + } else { + TunerHal hal = ((BaseTunerSetupActivity) mActivity).getTunerHal(); + if (hal == null) { + throw new RuntimeException("Failed to open a DVB device"); + } + mScanTsStreamer = new TunerTsStreamer(hal, this); + } + mFileTsStreamer = SCAN_LOCAL_STREAMS ? new FileTsStreamer(this, mActivity) : null; + mConditionStopped = new ConditionVariable(); + mChannelDataManager.setChannelScanListener(this, new Handler()); + } + + private void maybeSetChannelListVisible() { + mActivity.runOnUiThread( + new Runnable() { + @Override + public void run() { + int channelsFound = mAdapter.getCount(); + if (!mChannelListVisible && channelsFound > 0) { + String format = + getResources() + .getQuantityString( + R.plurals.ut_channel_scan_message, + channelsFound, + channelsFound); + mScanningMessage.setText(String.format(format, channelsFound)); + mChannelHolder.setVisibility(View.VISIBLE); + mChannelListVisible = true; + } + } + }); + } + + private void addChannel(final TunerChannel channel) { + mActivity.runOnUiThread( + new Runnable() { + @Override + public void run() { + mAdapter.add(channel); + if (mChannelListVisible) { + int channelsFound = mAdapter.getCount(); + String format = + getResources() + .getQuantityString( + R.plurals.ut_channel_scan_message, + channelsFound, + channelsFound); + mScanningMessage.setText(String.format(format, channelsFound)); + } + } + }); + } + + @Override + protected Void doInBackground(Void... params) { + mScanChannelList.clear(); + if (SCAN_LOCAL_STREAMS) { + FileTsStreamer.addLocalStreamFiles(mScanChannelList); + } + mScanChannelList.addAll( + ChannelScanFileParser.parseScanFile( + getResources().openRawResource(mChannelMapId))); + scanChannels(); + return null; + } + + @Override + protected void onCancelled() { + SoftPreconditions.checkState(false, TAG, "call cancelScan instead of cancel"); + } + + @Override + protected void onProgressUpdate(Integer... values) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mProgressBar.setProgress(values[0], true); + } else { + mProgressBar.setProgress(values[0]); + } + } + + private void stopScan() { + if (mLatch != null) { + mLatch.countDown(); + } + mConditionStopped.open(); + } + + private void cancelScan(boolean cancel) { + mIsCanceled = cancel; + stopScan(); + } + + private void scanChannels() { + if (DEBUG) Log.i(TAG, "Channel scan starting"); + mChannelDataManager.notifyScanStarted(); + + long startMs = System.currentTimeMillis(); + int i = 1; + for (ChannelScanFileParser.ScanChannel scanChannel : mScanChannelList) { + int frequency = scanChannel.frequency; + String modulation = scanChannel.modulation; + Log.i(TAG, "Tuning to " + frequency + " " + modulation); + + TsStreamer streamer = getStreamer(scanChannel.type); + SoftPreconditions.checkNotNull(streamer); + if (streamer != null && streamer.startStream(scanChannel)) { + mLatch = new CountDownLatch(1); + try { + mLatch.await(CHANNEL_SCAN_PERIOD_MS, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Log.e( + TAG, + "The current thread is interrupted during scanChannels(). " + + "The TS stream is stopped earlier than expected.", + e); + } + streamer.stopStream(); + addChannelsWithoutVct(scanChannel); + if (System.currentTimeMillis() > startMs + CHANNEL_SCAN_SHOW_DELAY_MS + && !mChannelListVisible) { + maybeSetChannelListVisible(); + } + } + if (mConditionStopped.block(-1)) { + break; + } + publishProgress(MAX_PROGRESS * i++ / mScanChannelList.size()); + } + mChannelDataManager.notifyScanCompleted(); + if (!mConditionStopped.block(-1)) { + publishProgress(MAX_PROGRESS); + } + if (DEBUG) Log.i(TAG, "Channel scan ended"); + } + + private void addChannelsWithoutVct(ChannelScanFileParser.ScanChannel scanChannel) { + if (scanChannel.radioFrequencyNumber == null + || !(mScanTsStreamer instanceof TunerTsStreamer)) { + return; + } + for (TunerChannel tunerChannel : + ((TunerTsStreamer) mScanTsStreamer).getMalFormedChannels()) { + if ((tunerChannel.getVideoPid() != TunerChannel.INVALID_PID) + && (tunerChannel.getAudioPid() != TunerChannel.INVALID_PID)) { + tunerChannel.setFrequency(scanChannel.frequency); + tunerChannel.setModulation(scanChannel.modulation); + tunerChannel.setShortName( + String.format( + Locale.US, + VCTLESS_CHANNEL_NAME_FORMAT, + scanChannel.radioFrequencyNumber, + tunerChannel.getProgramNumber())); + tunerChannel.setVirtualMajor(scanChannel.radioFrequencyNumber); + tunerChannel.setVirtualMinor(tunerChannel.getProgramNumber()); + onChannelDetected(tunerChannel, true); + } + } + } + + private TsStreamer getStreamer(int type) { + switch (type) { + case Channel.TunerType.TYPE_TUNER: + return mScanTsStreamer; + case Channel.TunerType.TYPE_FILE: + return mFileTsStreamer; + default: + return null; + } + } + + @Override + public void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items) { + mChannelDataManager.notifyEventDetected(channel, items); + } + + @Override + public void onChannelScanDone() { + if (mLatch != null) { + mLatch.countDown(); + } + } + + @Override + public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) { + if (channelArrivedAtFirstTime) { + Log.i(TAG, "Found channel " + channel); + } + if (channelArrivedAtFirstTime && channel.hasAudio()) { + // Playbacks with video-only stream have not been tested yet. + // No video-only channel has been found. + addChannel(channel); + mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime); + mChannelNumbers.add(channel.getDisplayNumber()); + } + } + + public void showFinishingProgressDialog() { + // Show a progress dialog to wait for the scanning process if it's not done yet. + if (!mIsFinished && mFinishingProgressDialog == null) { + mFinishingProgressDialog = + ProgressDialog.show( + mActivity, "", getString(R.string.ut_setup_cancel), true, false); + } + } + + @Override + public void onChannelHandlingDone() { + mChannelDataManager.setCurrentVersion(mActivity); + mChannelDataManager.releaseSafely(); + mIsFinished = true; + TunerPreferences.setScannedChannelCount( + mActivity.getApplicationContext(), + mChannelDataManager.getScannedChannelCount()); + // Cancel a previously shown notification. + BaseTunerSetupActivity.cancelNotification(mActivity.getApplicationContext()); + // Mark scan as done + TunerPreferences.setScanDone(mActivity.getApplicationContext()); + // finishing will be done manually. + if (mFinishingProgressDialog != null) { + mFinishingProgressDialog.dismiss(); + } + // If the fragment is not resumed, the next fragment (scan result page) can't be + // displayed. In that case, just close the activity. + if (isResumed()) { + if (mIsCanceled) { + onActionClick(ACTION_CATEGORY, ACTION_CANCEL); + } else { + Bundle params = new Bundle(); + params.putStringArrayList(KEY_CHANNEL_NUMBERS, mChannelNumbers); + onActionClick(ACTION_CATEGORY, ACTION_FINISH, params); + } + } else if (getActivity() != null) { + getActivity().finish(); + } + mChannelScanTask = null; + } + } + + private static class FakeTsStreamer implements TsStreamer { + private final EventDetector.EventListener mEventListener; + private int mProgramNumber = 0; + + FakeTsStreamer(EventDetector.EventListener eventListener) { + mEventListener = eventListener; + } + + @Override + public boolean startStream(ChannelScanFileParser.ScanChannel channel) { + if (++mProgramNumber % 2 == 1) { + return true; + } + final String displayNumber = Integer.toString(mProgramNumber); + final String name = "Channel-" + mProgramNumber; + mEventListener.onChannelDetected( + new TunerChannel(mProgramNumber, new ArrayList<>()) { + @Override + public String getDisplayNumber() { + return displayNumber; + } + + @Override + public String getName() { + return name; + } + }, + true); + return true; + } + + @Override + public boolean startStream(TunerChannel channel) { + return false; + } + + @Override + public void stopStream() {} + + @Override + public TsDataSource createDataSource() { + return null; + } + } +} |