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