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