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