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
17package com.android.usbtuner.setup;
18
19import android.animation.LayoutTransition;
20import android.app.Activity;
21import android.app.ProgressDialog;
22import android.content.Context;
23import android.os.AsyncTask;
24import android.os.Bundle;
25import android.os.ConditionVariable;
26import android.os.Handler;
27import android.util.Log;
28import android.view.LayoutInflater;
29import android.view.View;
30import android.view.View.OnClickListener;
31import android.view.ViewGroup;
32import android.widget.BaseAdapter;
33import android.widget.Button;
34import android.widget.ListView;
35import android.widget.ProgressBar;
36import android.widget.TextView;
37
38import com.android.tv.common.AutoCloseableUtils;
39import com.android.tv.common.ui.setup.SetupFragment;
40import com.android.usbtuner.ChannelScanFileParser;
41import com.android.usbtuner.ChannelScanFileParser.ScanChannel;
42import com.android.usbtuner.FileDataSource;
43import com.android.usbtuner.InputStreamSource;
44import com.android.usbtuner.R;
45import com.android.usbtuner.UsbTunerPreferences;
46import com.android.usbtuner.UsbTunerTsScannerSource;
47import com.android.usbtuner.data.Channel;
48import com.android.usbtuner.data.PsiData;
49import com.android.usbtuner.data.PsipData;
50import com.android.usbtuner.data.TunerChannel;
51import com.android.usbtuner.tvinput.ChannelDataManager;
52import com.android.usbtuner.tvinput.EventDetector;
53
54import junit.framework.Assert;
55
56import java.util.ArrayList;
57import java.util.List;
58
59/**
60 * A fragment for scanning channels.
61 */
62public 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
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
121    protected int getLayoutResourceId() {
122        return R.layout.ut_channel_scan;
123    }
124
125    @Override
126    protected int[] getParentIdsForDelay() {
127        return new int[] {R.id.progress_holder};
128    }
129
130    private void startScan(int channelMapId) {
131        mChannelScanTask = new ChannelScanTask(channelMapId);
132        mChannelScanTask.execute();
133    }
134
135    @Override
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     */
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
167        public ChannelAdapter() {
168            mChannels = new ArrayList<>();
169        }
170
171        @Override
172        public boolean areAllItemsEnabled() {
173            return false;
174        }
175
176        @Override
177        public boolean isEnabled(int pos) {
178            return false;
179        }
180
181        @Override
182        public int getCount() {
183            return mChannels.size();
184        }
185
186        @Override
187        public Object getItem(int pos) {
188            return pos;
189        }
190
191        @Override
192        public long getItemId(int pos) {
193            return pos;
194        }
195
196        @Override
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
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
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
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
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
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
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
312        protected void onProgressUpdate(Integer... values) {
313            mProgressBar.setProgress(values[0]);
314        }
315
316        private void stopScan() {
317            mConditionStopped.open();
318        }
319
320        private void cancelScan(boolean cancel) {
321            mIsCanceled = cancel;
322            stopScan();
323        }
324
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
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
375        public void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items) {
376            mChannelDataManager.notifyEventDetected(channel, items);
377        }
378
379        @Override
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
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
403        FakeInputStreamSource(EventDetector.EventListener eventListener) {
404            mEventListener = eventListener;
405        }
406
407        @Override
408        public int getType() {
409            return 0;
410        }
411
412        @Override
413        public boolean setScanChannel(ScanChannel channel) {
414            return true;
415        }
416
417        @Override
418        public boolean tuneToChannel(TunerChannel channel) {
419            return false;
420        }
421
422        @Override
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
444        public void stopStream() {
445        }
446
447        @Override
448        public long getLimit() {
449            return 0;
450        }
451
452        @Override
453        public long getPosition() {
454            return 0;
455        }
456
457        @Override
458        public void close() {
459        }
460    }
461}
462