Home | History | Annotate | Download | only in setup
      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.Build;
     25 import android.os.Bundle;
     26 import android.os.ConditionVariable;
     27 import android.os.Handler;
     28 import android.util.Log;
     29 import android.view.LayoutInflater;
     30 import android.view.View;
     31 import android.view.View.OnClickListener;
     32 import android.view.ViewGroup;
     33 import android.widget.BaseAdapter;
     34 import android.widget.Button;
     35 import android.widget.ListView;
     36 import android.widget.ProgressBar;
     37 import android.widget.TextView;
     38 
     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.R;
     43 import com.android.tv.tuner.TunerHal;
     44 import com.android.tv.tuner.TunerPreferences;
     45 import com.android.tv.tuner.data.PsipData;
     46 import com.android.tv.tuner.data.TunerChannel;
     47 import com.android.tv.tuner.data.nano.Channel;
     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 
     55 import java.util.ArrayList;
     56 import java.util.List;
     57 import java.util.Locale;
     58 import java.util.concurrent.CountDownLatch;
     59 import java.util.concurrent.TimeUnit;
     60 
     61 /**
     62  * A fragment for scanning channels.
     63  */
     64 public class ScanFragment extends SetupFragment {
     65     private static final String TAG = "ScanFragment";
     66     private static final boolean DEBUG = false;
     67 
     68     // In the fake mode, the connection to antenna or cable is not necessary.
     69     // Instead dummy channels are added.
     70     private static final boolean FAKE_MODE = false;
     71 
     72     private static final String VCTLESS_CHANNEL_NAME_FORMAT = "RF%d-%d";
     73 
     74     public static final String ACTION_CATEGORY = "com.android.tv.tuner.setup.ScanFragment";
     75     public static final int ACTION_CANCEL = 1;
     76     public static final int ACTION_FINISH = 2;
     77 
     78     public static final String EXTRA_FOR_CHANNEL_SCAN_FILE = "scan_file_choice";
     79 
     80     private static final long CHANNEL_SCAN_SHOW_DELAY_MS = 10000;
     81     private static final long CHANNEL_SCAN_PERIOD_MS = 4000;
     82     private static final long SHOW_PROGRESS_DIALOG_DELAY_MS = 300;
     83 
     84     // Build channels out of the locally stored TS streams.
     85     private static final boolean SCAN_LOCAL_STREAMS = true;
     86 
     87     private ChannelDataManager mChannelDataManager;
     88     private ChannelScanTask mChannelScanTask;
     89     private ProgressBar mProgressBar;
     90     private TextView mScanningMessage;
     91     private View mChannelHolder;
     92     private ChannelAdapter mAdapter;
     93     private volatile boolean mChannelListVisible;
     94     private Button mCancelButton;
     95 
     96     @Override
     97     public View onCreateView(LayoutInflater inflater, ViewGroup container,
     98             Bundle savedInstanceState) {
     99         if (DEBUG) Log.d(TAG, "onCreateView");
    100         View view = super.onCreateView(inflater, container, savedInstanceState);
    101         mChannelDataManager = new ChannelDataManager(getActivity());
    102         mChannelDataManager.checkDataVersion(getActivity());
    103         mAdapter = new ChannelAdapter();
    104         mProgressBar = (ProgressBar) view.findViewById(R.id.tune_progress);
    105         mScanningMessage = (TextView) view.findViewById(R.id.tune_description);
    106         ListView channelList = (ListView) view.findViewById(R.id.channel_list);
    107         channelList.setAdapter(mAdapter);
    108         channelList.setOnItemClickListener(null);
    109         ViewGroup progressHolder = (ViewGroup) view.findViewById(R.id.progress_holder);
    110         LayoutTransition transition = new LayoutTransition();
    111         transition.enableTransitionType(LayoutTransition.CHANGING);
    112         progressHolder.setLayoutTransition(transition);
    113         mChannelHolder = view.findViewById(R.id.channel_holder);
    114         mCancelButton = (Button) view.findViewById(R.id.tune_cancel);
    115         mCancelButton.setOnClickListener(new OnClickListener() {
    116             @Override
    117             public void onClick(View v) {
    118                 finishScan(false);
    119             }
    120         });
    121         Bundle args = getArguments();
    122         int tunerType = (args == null ? 0 : args.getInt(TunerSetupActivity.KEY_TUNER_TYPE, 0));
    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         switch (tunerType) {
    127             case TunerHal.TUNER_TYPE_USB:
    128                 scanTitleView.setText(R.string.ut_channel_scan);
    129                 break;
    130             case TunerHal.TUNER_TYPE_NETWORK:
    131                 scanTitleView.setText(R.string.nt_channel_scan);
    132                 break;
    133             default:
    134                 scanTitleView.setText(R.string.bt_channel_scan);
    135         }
    136         return view;
    137     }
    138 
    139     @Override
    140     protected int getLayoutResourceId() {
    141         return R.layout.ut_channel_scan;
    142     }
    143 
    144     @Override
    145     protected int[] getParentIdsForDelay() {
    146         return new int[] {R.id.progress_holder};
    147     }
    148 
    149     private void startScan(int channelMapId) {
    150         mChannelScanTask = new ChannelScanTask(channelMapId);
    151         mChannelScanTask.execute();
    152     }
    153 
    154     @Override
    155     public void onPause() {
    156         Log.d(TAG, "onPause");
    157         if (mChannelScanTask != null) {
    158             // Ensure scan task will stop.
    159             Log.w(TAG, "The activity went to the background. Stopping channel scan.");
    160             mChannelScanTask.stopScan();
    161         }
    162         super.onPause();
    163     }
    164 
    165     /**
    166      * Finishes the current scan thread. This fragment will be popped after the scan thread ends.
    167      *
    168      * @param cancel a flag which indicates the scan is canceled or not.
    169      */
    170     public void finishScan(boolean cancel) {
    171         if (mChannelScanTask != null) {
    172             mChannelScanTask.cancelScan(cancel);
    173 
    174             // Notifies a user of waiting to finish the scanning process.
    175             new Handler().postDelayed(new Runnable() {
    176                 @Override
    177                 public void run() {
    178                     if (mChannelScanTask != null) {
    179                         mChannelScanTask.showFinishingProgressDialog();
    180                     }
    181                 }
    182             }, SHOW_PROGRESS_DIALOG_DELAY_MS);
    183 
    184             // Hides the cancel button.
    185             mCancelButton.setEnabled(false);
    186         }
    187     }
    188 
    189     private class ChannelAdapter extends BaseAdapter {
    190         private final ArrayList<TunerChannel> mChannels;
    191 
    192         public ChannelAdapter() {
    193             mChannels = new ArrayList<>();
    194         }
    195 
    196         @Override
    197         public boolean areAllItemsEnabled() {
    198             return false;
    199         }
    200 
    201         @Override
    202         public boolean isEnabled(int pos) {
    203             return false;
    204         }
    205 
    206         @Override
    207         public int getCount() {
    208             return mChannels.size();
    209         }
    210 
    211         @Override
    212         public Object getItem(int pos) {
    213             return pos;
    214         }
    215 
    216         @Override
    217         public long getItemId(int pos) {
    218             return pos;
    219         }
    220 
    221         @Override
    222         public View getView(int position, View convertView, ViewGroup parent) {
    223             final Context context = parent.getContext();
    224 
    225             if (convertView == null) {
    226                 LayoutInflater inflater = (LayoutInflater) context
    227                         .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    228                 convertView = inflater.inflate(R.layout.ut_channel_list, parent, false);
    229             }
    230 
    231             TextView channelNum = (TextView) convertView.findViewById(R.id.channel_num);
    232             channelNum.setText(mChannels.get(position).getDisplayNumber());
    233 
    234             TextView channelName = (TextView) convertView.findViewById(R.id.channel_name);
    235             channelName.setText(mChannels.get(position).getName());
    236             return convertView;
    237         }
    238 
    239         public void add(TunerChannel channel) {
    240             mChannels.add(channel);
    241             notifyDataSetChanged();
    242         }
    243     }
    244 
    245     private class ChannelScanTask extends AsyncTask<Void, Integer, Void>
    246             implements EventDetector.EventListener, ChannelDataManager.ChannelScanListener {
    247         private static final int MAX_PROGRESS = 100;
    248 
    249         private final Activity mActivity;
    250         private final int mChannelMapId;
    251         private final TsStreamer mScanTsStreamer;
    252         private final TsStreamer mFileTsStreamer;
    253         private final ConditionVariable mConditionStopped;
    254 
    255         private final List<ChannelScanFileParser.ScanChannel> mScanChannelList = new ArrayList<>();
    256         private boolean mIsCanceled;
    257         private boolean mIsFinished;
    258         private ProgressDialog mFinishingProgressDialog;
    259         private CountDownLatch mLatch;
    260 
    261         public ChannelScanTask(int channelMapId) {
    262             mActivity = getActivity();
    263             mChannelMapId = channelMapId;
    264             if (FAKE_MODE) {
    265                 mScanTsStreamer = new FakeTsStreamer(this);
    266             } else {
    267                 TunerHal hal = ((TunerSetupActivity) mActivity).getTunerHal();
    268                 if (hal == null) {
    269                     throw new RuntimeException("Failed to open a DVB device");
    270                 }
    271                 mScanTsStreamer = new TunerTsStreamer(hal, this);
    272             }
    273             mFileTsStreamer = SCAN_LOCAL_STREAMS ? new FileTsStreamer(this, mActivity) : null;
    274             mConditionStopped = new ConditionVariable();
    275             mChannelDataManager.setChannelScanListener(this, new Handler());
    276         }
    277 
    278         private void maybeSetChannelListVisible() {
    279             mActivity.runOnUiThread(new Runnable() {
    280                 @Override
    281                 public void run() {
    282                     int channelsFound = mAdapter.getCount();
    283                     if (!mChannelListVisible && channelsFound > 0) {
    284                         String format = getResources().getQuantityString(
    285                                 R.plurals.ut_channel_scan_message, channelsFound, channelsFound);
    286                         mScanningMessage.setText(String.format(format, channelsFound));
    287                         mChannelHolder.setVisibility(View.VISIBLE);
    288                         mChannelListVisible = true;
    289                     }
    290                 }
    291             });
    292         }
    293 
    294         private void addChannel(final TunerChannel channel) {
    295             mActivity.runOnUiThread(new Runnable() {
    296                 @Override
    297                 public void run() {
    298                     mAdapter.add(channel);
    299                     if (mChannelListVisible) {
    300                         int channelsFound = mAdapter.getCount();
    301                         String format = getResources().getQuantityString(
    302                                 R.plurals.ut_channel_scan_message, channelsFound, channelsFound);
    303                         mScanningMessage.setText(String.format(format, channelsFound));
    304                     }
    305                 }
    306             });
    307         }
    308 
    309         @Override
    310         protected Void doInBackground(Void... params) {
    311             mScanChannelList.clear();
    312             if (SCAN_LOCAL_STREAMS) {
    313                 FileTsStreamer.addLocalStreamFiles(mScanChannelList);
    314             }
    315             mScanChannelList.addAll(ChannelScanFileParser.parseScanFile(
    316                     getResources().openRawResource(mChannelMapId)));
    317             scanChannels();
    318             return null;
    319         }
    320 
    321         @Override
    322         protected void onCancelled() {
    323             SoftPreconditions.checkState(false, TAG, "call cancelScan instead of cancel");
    324         }
    325 
    326         @Override
    327         protected void onProgressUpdate(Integer... values) {
    328             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    329                 mProgressBar.setProgress(values[0], true);
    330             } else {
    331                 mProgressBar.setProgress(values[0]);
    332             }
    333         }
    334 
    335         private void stopScan() {
    336             if (mLatch != null) {
    337                 mLatch.countDown();
    338             }
    339             mConditionStopped.open();
    340         }
    341 
    342         private void cancelScan(boolean cancel) {
    343             mIsCanceled = cancel;
    344             stopScan();
    345         }
    346 
    347         private void scanChannels() {
    348             if (DEBUG) Log.i(TAG, "Channel scan starting");
    349             mChannelDataManager.notifyScanStarted();
    350 
    351             long startMs = System.currentTimeMillis();
    352             int i = 1;
    353             for (ChannelScanFileParser.ScanChannel scanChannel : mScanChannelList) {
    354                 int frequency = scanChannel.frequency;
    355                 String modulation = scanChannel.modulation;
    356                 Log.i(TAG, "Tuning to " + frequency + " " + modulation);
    357 
    358                 TsStreamer streamer = getStreamer(scanChannel.type);
    359                 SoftPreconditions.checkNotNull(streamer);
    360                 if (streamer != null && streamer.startStream(scanChannel)) {
    361                     mLatch = new CountDownLatch(1);
    362                     try {
    363                         mLatch.await(CHANNEL_SCAN_PERIOD_MS, TimeUnit.MILLISECONDS);
    364                     } catch (InterruptedException e) {
    365                         Log.e(TAG, "The current thread is interrupted during scanChannels(). " +
    366                                 "The TS stream is stopped earlier than expected.", e);
    367                     }
    368                     streamer.stopStream();
    369 
    370                     addChannelsWithoutVct(scanChannel);
    371                     if (System.currentTimeMillis() > startMs + CHANNEL_SCAN_SHOW_DELAY_MS
    372                             && !mChannelListVisible) {
    373                         maybeSetChannelListVisible();
    374                     }
    375                 }
    376                 if (mConditionStopped.block(-1)) {
    377                     break;
    378                 }
    379                 publishProgress(MAX_PROGRESS * i++ / mScanChannelList.size());
    380             }
    381             mChannelDataManager.notifyScanCompleted();
    382             if (!mConditionStopped.block(-1)) {
    383                 publishProgress(MAX_PROGRESS);
    384             }
    385             if (DEBUG) Log.i(TAG, "Channel scan ended");
    386         }
    387 
    388 
    389         private void addChannelsWithoutVct(ChannelScanFileParser.ScanChannel scanChannel) {
    390             if (scanChannel.radioFrequencyNumber == null
    391                     || !(mScanTsStreamer instanceof TunerTsStreamer)) {
    392                 return;
    393             }
    394             for (TunerChannel tunerChannel
    395                     : ((TunerTsStreamer) mScanTsStreamer).getMalFormedChannels()) {
    396                 if ((tunerChannel.getVideoPid() != TunerChannel.INVALID_PID)
    397                         && (tunerChannel.getAudioPid() != TunerChannel.INVALID_PID)) {
    398                     tunerChannel.setFrequency(scanChannel.frequency);
    399                     tunerChannel.setModulation(scanChannel.modulation);
    400                     tunerChannel.setShortName(String.format(Locale.US, VCTLESS_CHANNEL_NAME_FORMAT,
    401                             scanChannel.radioFrequencyNumber,
    402                             tunerChannel.getProgramNumber()));
    403                     tunerChannel.setVirtualMajor(scanChannel.radioFrequencyNumber);
    404                     tunerChannel.setVirtualMinor(tunerChannel.getProgramNumber());
    405                     onChannelDetected(tunerChannel, true);
    406                 }
    407             }
    408         }
    409 
    410         private TsStreamer getStreamer(int type) {
    411             switch (type) {
    412                 case Channel.TYPE_TUNER:
    413                     return mScanTsStreamer;
    414                 case Channel.TYPE_FILE:
    415                     return mFileTsStreamer;
    416                 default:
    417                     return null;
    418             }
    419         }
    420 
    421         @Override
    422         public void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items) {
    423             mChannelDataManager.notifyEventDetected(channel, items);
    424         }
    425 
    426         @Override
    427         public void onChannelScanDone() {
    428             if (mLatch != null) {
    429                 mLatch.countDown();
    430             }
    431         }
    432 
    433         @Override
    434         public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) {
    435             if (channelArrivedAtFirstTime) {
    436                 Log.i(TAG, "Found channel " + channel);
    437             }
    438             if (channelArrivedAtFirstTime && channel.hasAudio()) {
    439                 // Playbacks with video-only stream have not been tested yet.
    440                 // No video-only channel has been found.
    441                 addChannel(channel);
    442                 mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime);
    443             }
    444         }
    445 
    446         public void showFinishingProgressDialog() {
    447             // Show a progress dialog to wait for the scanning process if it's not done yet.
    448             if (!mIsFinished && mFinishingProgressDialog == null) {
    449                 mFinishingProgressDialog = ProgressDialog.show(mActivity, "",
    450                         getString(R.string.ut_setup_cancel), true, false);
    451             }
    452         }
    453 
    454         @Override
    455         public void onChannelHandlingDone() {
    456             mChannelDataManager.setCurrentVersion(mActivity);
    457             mChannelDataManager.releaseSafely();
    458             mIsFinished = true;
    459             TunerPreferences.setScannedChannelCount(mActivity.getApplicationContext(),
    460                     mChannelDataManager.getScannedChannelCount());
    461             // Cancel a previously shown notification.
    462             TunerSetupActivity.cancelNotification(mActivity.getApplicationContext());
    463             // Mark scan as done
    464             TunerPreferences.setScanDone(mActivity.getApplicationContext());
    465             // finishing will be done manually.
    466             if (mFinishingProgressDialog != null) {
    467                 mFinishingProgressDialog.dismiss();
    468             }
    469             // If the fragment is not resumed, the next fragment (scan result page) can't be
    470             // displayed. In that case, just close the activity.
    471             if (isResumed()) {
    472                 onActionClick(ACTION_CATEGORY, mIsCanceled ? ACTION_CANCEL : ACTION_FINISH);
    473             } else if (getActivity() != null) {
    474                 getActivity().finish();
    475             }
    476             mChannelScanTask = null;
    477         }
    478     }
    479 
    480     private static class FakeTsStreamer implements TsStreamer {
    481         private final EventDetector.EventListener mEventListener;
    482         private int mProgramNumber = 0;
    483 
    484         FakeTsStreamer(EventDetector.EventListener eventListener) {
    485             mEventListener = eventListener;
    486         }
    487 
    488         @Override
    489         public boolean startStream(ChannelScanFileParser.ScanChannel channel) {
    490             if (++mProgramNumber % 2 == 1) {
    491                 return true;
    492             }
    493             final String displayNumber = Integer.toString(mProgramNumber);
    494             final String name = "Channel-" + mProgramNumber;
    495             mEventListener.onChannelDetected(new TunerChannel(mProgramNumber, new ArrayList<>()) {
    496                 @Override
    497                 public String getDisplayNumber() {
    498                     return displayNumber;
    499                 }
    500 
    501                 @Override
    502                 public String getName() {
    503                     return name;
    504                 }
    505             }, true);
    506             return true;
    507         }
    508 
    509         @Override
    510         public boolean startStream(TunerChannel channel) {
    511             return false;
    512         }
    513 
    514         @Override
    515         public void stopStream() {
    516         }
    517 
    518         @Override
    519         public TsDataSource createDataSource() {
    520             return null;
    521         }
    522     }
    523 }
    524