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