blob: 7d59284cec2817c5a1627739440b3a3ce33d03cd [file] [log] [blame]
/*
* 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;
}
}
}