1 /*
2  * Copyright (C) 2018 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.sample.dvb.setup;
18 
19 import android.app.FragmentManager;
20 import android.content.ContentResolver;
21 import android.content.ContentValues;
22 import android.content.Intent;
23 import android.content.pm.PackageManager;
24 import android.media.tv.TvInputInfo;
25 import android.os.AsyncTask;
26 import android.os.Bundle;
27 import android.os.Handler;
28 import android.support.annotation.Nullable;
29 import android.text.TextUtils;
30 import android.util.Log;
31 import android.util.Pair;
32 import android.view.KeyEvent;
33 import com.android.tv.common.feature.CommonFeatures;
34 import com.android.tv.common.singletons.HasSingletons;
35 import com.android.tv.common.ui.setup.SetupFragment;
36 import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
37 import com.android.tv.common.util.PostalCodeUtils;
38 import com.android.tv.tuner.sample.dvb.R;
39 import com.android.tv.tuner.sample.dvb.util.SampleDvbConstants;
40 import com.android.tv.tuner.setup.BaseTunerSetupActivity;
41 import com.android.tv.tuner.setup.ConnectionTypeFragment;
42 import com.android.tv.tuner.setup.LineupFragment;
43 import com.android.tv.tuner.setup.LocationFragment;
44 import com.android.tv.tuner.setup.PostalCodeFragment;
45 import com.android.tv.tuner.setup.ScanFragment;
46 import com.android.tv.tuner.setup.ScanResultFragment;
47 import com.android.tv.tuner.setup.WelcomeFragment;
48 import com.android.tv.tuner.singletons.TunerSingletons;
49 import com.google.android.tv.partner.support.EpgContract;
50 import com.google.android.tv.partner.support.EpgInput;
51 import com.google.android.tv.partner.support.EpgInputs;
52 import com.google.android.tv.partner.support.Lineup;
53 import com.google.android.tv.partner.support.Lineups;
54 import com.google.android.tv.partner.support.TunerSetupUtils;
55 import dagger.android.ContributesAndroidInjector;
56 import java.util.ArrayList;
57 import java.util.List;
58 
59 /** An activity that serves Sample DVB tuner setup process. */
60 public class SampleDvbTunerSetupActivity extends BaseTunerSetupActivity {
61     private static final String TAG = "SampleDvbTunerSetupActivity";
62     private static final boolean DEBUG = false;
63 
64     private static final int FETCH_LINEUP_TIMEOUT_MS = 10000; // 10 seconds
65     private static final int FETCH_LINEUP_RETRY_TIMEOUT_MS = 20000; // 20 seconds
66     private static final String OTAD_PREFIX = "OTAD";
67     private static final String STRING_BROADCAST_DIGITAL = "Broadcast Digital";
68 
69     private LineupFragment currentLineupFragment;
70 
71     private List<String> channelNumbers;
72     private List<Lineup> lineups;
73     private Lineup selectedLineup;
74     private List<Pair<Lineup, Integer>> lineupMatchCountPair;
75     private FetchLineupTask fetchLineupTask;
76     private EpgInput epgInput;
77     private String postalCode;
78     private final Handler handler = new Handler();
79     private final Runnable cancelFetchLineupTaskRunnable = this::cancelFetchLineup;
80     private String embeddedInputId;
81 
SampleDvbTunerSetupActivity()82     public SampleDvbTunerSetupActivity() {
83         super(SampleDvbConstants.TUNER_INPUT_ID);
84     }
85 
86     @Override
onCreate(Bundle savedInstanceState)87     protected void onCreate(Bundle savedInstanceState) {
88         super.onCreate(savedInstanceState);
89         if (DEBUG) {
90             Log.d(TAG, "onCreate");
91         }
92         embeddedInputId =
93                 HasSingletons.get(TunerSingletons.class, getApplicationContext())
94                         .getEmbeddedTunerInputId();
95         new QueryEpgInputTask(embeddedInputId).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
96     }
97 
98     @Override
executeGetTunerTypeAndCountAsyncTask()99     protected void executeGetTunerTypeAndCountAsyncTask() {
100         new AsyncTask<Void, Void, Integer>() {
101             @Override
102             protected Integer doInBackground(Void... arg0) {
103                 return mTunerFactory.getTunerTypeAndCount(SampleDvbTunerSetupActivity.this).first;
104             }
105 
106             @Override
107             protected void onPostExecute(Integer result) {
108                 if (!SampleDvbTunerSetupActivity.this.isDestroyed()) {
109                     mTunerType = result;
110                     if (result == null) {
111                         finish();
112                     } else if (!mActivityStopped) {
113                         showInitialFragment();
114                     } else {
115                         mPendingShowInitialFragment = true;
116                     }
117                 }
118             }
119         }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
120     }
121 
122     @Override
executeAction(String category, int actionId, Bundle params)123     protected boolean executeAction(String category, int actionId, Bundle params) {
124         switch (category) {
125             case WelcomeFragment.ACTION_CATEGORY:
126                 switch (actionId) {
127                     case SetupMultiPaneFragment.ACTION_DONE:
128                         super.executeAction(category, actionId, params);
129                         break;
130                     default:
131                         String postalCode = PostalCodeUtils.getLastPostalCode(this);
132                         boolean needLocation =
133                                 CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(
134                                                 getApplicationContext())
135                                         && TextUtils.isEmpty(postalCode);
136                         if (needLocation
137                                 && checkSelfPermission(
138                                                 android.Manifest.permission.ACCESS_COARSE_LOCATION)
139                                         != PackageManager.PERMISSION_GRANTED) {
140                             showLocationFragment();
141                         } else if (mNeedToShowPostalCodeFragment || needLocation) {
142                             // We cannot get postal code automatically. Postal code input fragment
143                             // should always be shown even if users have input some valid postal
144                             // code in this activity before.
145                             mNeedToShowPostalCodeFragment = true;
146                             showPostalCodeFragment();
147                         } else {
148                             lineups = null;
149                             selectedLineup = null;
150                             this.postalCode = postalCode;
151                             restartFetchLineupTask();
152                             showConnectionTypeFragment();
153                         }
154                         break;
155                 }
156                 return true;
157             case LocationFragment.ACTION_CATEGORY:
158                 switch (actionId) {
159                     case LocationFragment.ACTION_ALLOW_PERMISSION:
160                         String postalCode =
161                                 params == null
162                                         ? null
163                                         : params.getString(LocationFragment.KEY_POSTAL_CODE);
164                         if (postalCode == null) {
165                             showPostalCodeFragment();
166                         } else {
167                             this.postalCode = postalCode;
168                             restartFetchLineupTask();
169                             showConnectionTypeFragment();
170                         }
171                         break;
172                     default:
173                         cancelFetchLineup();
174                         showConnectionTypeFragment();
175                 }
176                 return true;
177             case PostalCodeFragment.ACTION_CATEGORY:
178                 lineups = null;
179                 selectedLineup = null;
180                 switch (actionId) {
181                     case SetupMultiPaneFragment.ACTION_DONE:
182                         String postalCode = params.getString(PostalCodeFragment.KEY_POSTAL_CODE);
183                         if (postalCode != null) {
184                             this.postalCode = postalCode;
185                             restartFetchLineupTask();
186                         }
187                         // fall through
188                     case SetupMultiPaneFragment.ACTION_SKIP:
189                         showConnectionTypeFragment();
190                         break;
191                     default: // fall out
192                 }
193                 return true;
194             case ConnectionTypeFragment.ACTION_CATEGORY:
195                 channelNumbers = null;
196                 lineupMatchCountPair = null;
197                 return super.executeAction(category, actionId, params);
198             case ScanFragment.ACTION_CATEGORY:
199                 switch (actionId) {
200                     case ScanFragment.ACTION_CANCEL:
201                         clearTunerHal();
202                         getFragmentManager().popBackStack();
203                         return true;
204                     case ScanFragment.ACTION_FINISH:
205                         clearTunerHal();
206                         channelNumbers =
207                                 params.getStringArrayList(ScanFragment.KEY_CHANNEL_NUMBERS);
208                         selectedLineup = null;
209                         if (CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(
210                                         getApplicationContext())
211                                 && channelNumbers != null
212                                 && !channelNumbers.isEmpty()
213                                 && !TextUtils.isEmpty(this.postalCode)) {
214                             showLineupFragment();
215                         } else {
216                             showScanResultFragment();
217                         }
218                         return true;
219                     default: // fall out
220                 }
221                 break;
222             case LineupFragment.ACTION_CATEGORY:
223                 switch (actionId) {
224                     case LineupFragment.ACTION_SKIP:
225                         selectedLineup = null;
226                         currentLineupFragment = null;
227                         showScanResultFragment();
228                         break;
229                     case LineupFragment.ACTION_ID_RETRY:
230                         currentLineupFragment.onRetry();
231                         restartFetchLineupTask();
232                         handler.postDelayed(
233                                 cancelFetchLineupTaskRunnable, FETCH_LINEUP_RETRY_TIMEOUT_MS);
234                         break;
235                     default:
236                         if (actionId >= 0 && actionId < lineupMatchCountPair.size()) {
237                             if (DEBUG) {
238                                 if (selectedLineup != null) {
239                                     Log.d(
240                                             TAG,
241                                             "Lineup " + selectedLineup.getName() + " is selected.");
242                                 }
243                             }
244                             selectedLineup = lineupMatchCountPair.get(actionId).first;
245                         }
246                         currentLineupFragment = null;
247                         showScanResultFragment();
248                         break;
249                 }
250                 return true;
251             case ScanResultFragment.ACTION_CATEGORY:
252                 switch (actionId) {
253                     case SetupMultiPaneFragment.ACTION_DONE:
254                         new InsertOrModifyEpgInputTask(selectedLineup, embeddedInputId)
255                                 .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
256                         break;
257                     default:
258                         // scan again
259                         if (lineups == null || lineups.isEmpty()) {
260                             lineups = null;
261                             restartFetchLineupTask();
262                         }
263                         super.executeAction(category, actionId, params);
264                         break;
265                 }
266                 return true;
267             default: // fall out
268         }
269         return false;
270     }
271 
272     @Override
onKeyUp(int keyCode, KeyEvent event)273     public boolean onKeyUp(int keyCode, KeyEvent event) {
274         if (keyCode == KeyEvent.KEYCODE_BACK) {
275             FragmentManager manager = getFragmentManager();
276             int count = manager.getBackStackEntryCount();
277             if (count > 0) {
278                 String lastTag = manager.getBackStackEntryAt(count - 1).getName();
279                 if (LineupFragment.class.getCanonicalName().equals(lastTag) && count >= 2) {
280                     // Pops fragment including ScanFragment.
281                     manager.popBackStack(
282                             manager.getBackStackEntryAt(count - 2).getName(),
283                             FragmentManager.POP_BACK_STACK_INCLUSIVE);
284                     return true;
285                 }
286                 if (ScanResultFragment.class.getCanonicalName().equals(lastTag) && count >= 2) {
287                     String secondLastTag = manager.getBackStackEntryAt(count - 2).getName();
288                     if (ScanFragment.class.getCanonicalName().equals(secondLastTag)) {
289                         // Pops fragment including ScanFragment.
290                         manager.popBackStack(
291                                 secondLastTag, FragmentManager.POP_BACK_STACK_INCLUSIVE);
292                         return true;
293                     }
294                     if (LineupFragment.class.getCanonicalName().equals(secondLastTag)) {
295                         currentLineupFragment =
296                                 (LineupFragment) manager.findFragmentByTag(secondLastTag);
297                         if (lineups == null || lineups.isEmpty()) {
298                             lineups = null;
299                             restartFetchLineupTask();
300                         }
301                     }
302                 } else if (ScanFragment.class.getCanonicalName().equals(lastTag)) {
303                     mLastScanFragment.finishScan(true);
304                     return true;
305                 }
306             }
307         }
308         return super.onKeyUp(keyCode, event);
309     }
310 
showLineupFragment()311     private void showLineupFragment() {
312         if (lineupMatchCountPair == null && lineups != null) {
313             lineupMatchCountPair = TunerSetupUtils.lineupChannelMatchCount(lineups, channelNumbers);
314         }
315         currentLineupFragment = new LineupFragment();
316         currentLineupFragment.setArguments(getArgsForLineupFragment());
317         currentLineupFragment.setShortDistance(
318                 SetupFragment.FRAGMENT_ENTER_TRANSITION | SetupFragment.FRAGMENT_RETURN_TRANSITION);
319         handler.removeCallbacksAndMessages(null);
320         showFragment(currentLineupFragment, true);
321         handler.postDelayed(cancelFetchLineupTaskRunnable, FETCH_LINEUP_TIMEOUT_MS);
322     }
323 
getArgsForLineupFragment()324     private Bundle getArgsForLineupFragment() {
325         Bundle args = new Bundle();
326         if (lineupMatchCountPair == null) {
327             return args;
328         }
329         ArrayList<String> lineupNames = new ArrayList<>(lineupMatchCountPair.size());
330         ArrayList<Integer> matchNumbers = new ArrayList<>(lineupMatchCountPair.size());
331         int defaultLineupIndex = 0;
332         for (Pair<Lineup, Integer> pair : lineupMatchCountPair) {
333             Lineup lineup = pair.first;
334             String name;
335             if (!TextUtils.isEmpty(lineup.getName())) {
336                 name = lineup.getName();
337             } else {
338                 name = lineup.getId();
339             }
340             if (name.equals(OTAD_PREFIX + postalCode) || name.equals(STRING_BROADCAST_DIGITAL)) {
341                 // rename OTA / antenna lineups
342                 name = getString(R.string.ut_lineup_name_antenna);
343             }
344             lineupNames.add(name);
345             matchNumbers.add(pair.second);
346             if (epgInput != null && TextUtils.equals(lineup.getId(), epgInput.getLineupId())) {
347                 // The last index is the current one.
348                 defaultLineupIndex = lineupNames.size() - 1;
349             }
350         }
351         args.putStringArrayList(LineupFragment.KEY_LINEUP_NAMES, lineupNames);
352         args.putIntegerArrayList(LineupFragment.KEY_MATCH_NUMBERS, matchNumbers);
353         args.putInt(LineupFragment.KEY_DEFAULT_LINEUP, defaultLineupIndex);
354         return args;
355     }
356 
cancelFetchLineup()357     private void cancelFetchLineup() {
358         if (fetchLineupTask == null) {
359             return;
360         }
361         AsyncTask.Status status = fetchLineupTask.getStatus();
362         if (status == AsyncTask.Status.RUNNING || status == AsyncTask.Status.PENDING) {
363             fetchLineupTask.cancel(true);
364             fetchLineupTask = null;
365             if (currentLineupFragment != null) {
366                 currentLineupFragment.onLineupNotFound();
367             }
368         }
369     }
370 
restartFetchLineupTask()371     private void restartFetchLineupTask() {
372         if (!CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(getApplicationContext())
373                 || TextUtils.isEmpty(postalCode)
374                 || checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION)
375                         != PackageManager.PERMISSION_GRANTED) {
376             return;
377         }
378         if (fetchLineupTask != null) {
379             fetchLineupTask.cancel(true);
380         }
381         handler.removeCallbacksAndMessages(null);
382         fetchLineupTask = new FetchLineupTask(getContentResolver(), postalCode);
383         fetchLineupTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
384     }
385 
386     private class FetchLineupTask extends AsyncTask<Void, Void, List<Lineup>> {
387         private final ContentResolver contentResolver;
388         private final String postalCode;
389 
FetchLineupTask(ContentResolver contentResolver, String postalCode)390         FetchLineupTask(ContentResolver contentResolver, String postalCode) {
391             this.contentResolver = contentResolver;
392             this.postalCode = postalCode;
393         }
394 
395         @Override
doInBackground(Void... args)396         protected List<Lineup> doInBackground(Void... args) {
397             if (contentResolver == null || TextUtils.isEmpty(postalCode)) {
398                 return new ArrayList<>();
399             }
400             return new ArrayList<>(Lineups.query(contentResolver, postalCode));
401         }
402 
403         @Override
onPostExecute(List<Lineup> lineups)404         protected void onPostExecute(List<Lineup> lineups) {
405             if (DEBUG) {
406                 if (lineups != null) {
407                     Log.d(TAG, "FetchLineupTask fetched " + lineups.size() + " lineups");
408                 } else {
409                     Log.d(TAG, "FetchLineupTask returned null");
410                 }
411             }
412             SampleDvbTunerSetupActivity.this.lineups = lineups;
413             if (currentLineupFragment != null) {
414                 if (lineups == null || lineups.isEmpty()) {
415                     currentLineupFragment.onLineupNotFound();
416                 } else {
417                     lineupMatchCountPair =
418                             TunerSetupUtils.lineupChannelMatchCount(
419                                     SampleDvbTunerSetupActivity.this.lineups, channelNumbers);
420                     currentLineupFragment.onLineupFound(getArgsForLineupFragment());
421                 }
422             }
423         }
424     }
425 
426     private class InsertOrModifyEpgInputTask extends AsyncTask<Void, Void, Void> {
427         private final Lineup lineup;
428         private final String inputId;
429 
InsertOrModifyEpgInputTask(@ullable Lineup lineup, String inputId)430         InsertOrModifyEpgInputTask(@Nullable Lineup lineup, String inputId) {
431             this.lineup = lineup;
432             this.inputId = inputId;
433         }
434 
435         @Override
doInBackground(Void... args)436         protected Void doInBackground(Void... args) {
437             if (lineup == null
438                     || (SampleDvbTunerSetupActivity.this.epgInput != null
439                             && TextUtils.equals(
440                                     lineup.getId(),
441                                     SampleDvbTunerSetupActivity.this.epgInput.getLineupId()))) {
442                 return null;
443             }
444             ContentValues values = new ContentValues();
445             values.put(EpgContract.EpgInputs.COLUMN_INPUT_ID, inputId);
446             values.put(EpgContract.EpgInputs.COLUMN_LINEUP_ID, lineup.getId());
447 
448             ContentResolver contentResolver = getContentResolver();
449             if (SampleDvbTunerSetupActivity.this.epgInput != null) {
450                 values.put(
451                         EpgContract.EpgInputs.COLUMN_ID,
452                         SampleDvbTunerSetupActivity.this.epgInput.getId());
453                 EpgInputs.update(contentResolver, EpgInput.createEpgChannel(values));
454                 return null;
455             }
456             EpgInput epgInput = EpgInputs.queryEpgInput(contentResolver, inputId);
457             if (epgInput == null) {
458                 contentResolver.insert(EpgContract.EpgInputs.CONTENT_URI, values);
459             } else {
460                 values.put(EpgContract.EpgInputs.COLUMN_ID, epgInput.getId());
461                 EpgInputs.update(contentResolver, EpgInput.createEpgChannel(values));
462             }
463             return null;
464         }
465 
466         @Override
onPostExecute(Void result)467         protected void onPostExecute(Void result) {
468             Intent data = new Intent();
469             data.putExtra(TvInputInfo.EXTRA_INPUT_ID, inputId);
470             data.putExtra(EpgContract.EXTRA_USE_CLOUD_EPG, true);
471             setResult(RESULT_OK, data);
472             finish();
473         }
474     }
475 
476     /**
477      * Exports {@link SampleDvbTunerSetupActivity} for Dagger codegen to create the appropriate
478      * injector.
479      */
480     @dagger.Module
481     public abstract static class Module {
482         @ContributesAndroidInjector
contributeSampleDvbTunerSetupActivityInjector()483         abstract SampleDvbTunerSetupActivity contributeSampleDvbTunerSetupActivityInjector();
484     }
485 
486     private class QueryEpgInputTask extends AsyncTask<Void, Void, EpgInput> {
487         private final String inputId;
488 
QueryEpgInputTask(String inputId)489         QueryEpgInputTask(String inputId) {
490             this.inputId = inputId;
491         }
492 
493         @Override
doInBackground(Void... args)494         protected EpgInput doInBackground(Void... args) {
495             ContentResolver contentResolver = getContentResolver();
496             return EpgInputs.queryEpgInput(contentResolver, inputId);
497         }
498 
499         @Override
onPostExecute(EpgInput result)500         protected void onPostExecute(EpgInput result) {
501             epgInput = result;
502         }
503     }
504 }
505