| /* |
| * 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.R; |
| import com.android.tv.tuner.api.ScanChannel; |
| import com.android.tv.tuner.api.Tuner; |
| 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.prefs.TunerPreferences; |
| 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.ts.EventDetector; |
| import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager; |
| 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().getApplicationContext()); |
| 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 Tuner.TUNER_TYPE_USB: |
| scanTitleView.setText(R.string.ut_channel_scan); |
| break; |
| case Tuner.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( |
| () -> { |
| 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.ChannelHandlingDoneListener { |
| 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<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 { |
| Tuner 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( |
| () -> { |
| 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( |
| () -> { |
| 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 (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(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(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; |
| } |
| } |
| } |