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.tv.tuner.setup; 18 19 import android.app.Fragment; 20 import android.app.FragmentManager; 21 import android.app.Notification; 22 import android.app.NotificationChannel; 23 import android.app.NotificationManager; 24 import android.app.PendingIntent; 25 import android.content.ComponentName; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.pm.PackageManager; 29 import android.content.res.Resources; 30 import android.graphics.Bitmap; 31 import android.graphics.BitmapFactory; 32 import android.media.tv.TvContract; 33 import android.os.AsyncTask; 34 import android.os.Build; 35 import android.os.Bundle; 36 import android.support.annotation.MainThread; 37 import android.support.annotation.NonNull; 38 import android.support.annotation.VisibleForTesting; 39 import android.support.annotation.WorkerThread; 40 import android.support.v4.app.NotificationCompat; 41 import android.text.TextUtils; 42 import android.util.Log; 43 import android.view.KeyEvent; 44 import android.widget.Toast; 45 46 import com.android.tv.Features; 47 import com.android.tv.TvApplication; 48 import com.android.tv.common.AutoCloseableUtils; 49 import com.android.tv.common.SoftPreconditions; 50 import com.android.tv.common.TvCommonConstants; 51 import com.android.tv.common.TvCommonUtils; 52 import com.android.tv.common.ui.setup.SetupActivity; 53 import com.android.tv.common.ui.setup.SetupFragment; 54 import com.android.tv.common.ui.setup.SetupMultiPaneFragment; 55 import com.android.tv.experiments.Experiments; 56 import com.android.tv.tuner.R; 57 import com.android.tv.tuner.TunerHal; 58 import com.android.tv.tuner.TunerPreferences; 59 import com.android.tv.tuner.tvinput.TunerTvInputService; 60 import com.android.tv.tuner.util.PostalCodeUtils; 61 62 import java.util.concurrent.Executor; 63 64 /** 65 * An activity that serves tuner setup process. 66 */ 67 public class TunerSetupActivity extends SetupActivity { 68 private static final String TAG = "TunerSetupActivity"; 69 private static final boolean DEBUG = false; 70 71 /** 72 * Key for passing tuner type to sub-fragments. 73 */ 74 public static final String KEY_TUNER_TYPE = "TunerSetupActivity.tunerType"; 75 76 // For the notification. 77 private static final String TV_ACTIVITY_CLASS_NAME = "com.android.tv.TvActivity"; 78 private static final String TUNER_SET_UP_NOTIFICATION_CHANNEL_ID = "tuner_setup_channel"; 79 private static final String NOTIFY_TAG = "TunerSetup"; 80 private static final int NOTIFY_ID = 1000; 81 private static final String TAG_DRAWABLE = "drawable"; 82 private static final String TAG_ICON = "ic_launcher_s"; 83 private static final int PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION = 1; 84 85 private static final int CHANNEL_MAP_SCAN_FILE[] = { 86 R.raw.ut_us_atsc_center_frequencies_8vsb, 87 R.raw.ut_us_cable_standard_center_frequencies_qam256, 88 R.raw.ut_us_all, 89 R.raw.ut_kr_atsc_center_frequencies_8vsb, 90 R.raw.ut_kr_cable_standard_center_frequencies_qam256, 91 R.raw.ut_kr_all, 92 R.raw.ut_kr_dev_cj_cable_center_frequencies_qam256, 93 R.raw.ut_euro_dvbt_all, 94 R.raw.ut_euro_dvbt_all, 95 R.raw.ut_euro_dvbt_all 96 }; 97 98 private ScanFragment mLastScanFragment; 99 private Integer mTunerType; 100 private TunerHalFactory mTunerHalFactory; 101 private boolean mNeedToShowPostalCodeFragment; 102 private String mPreviousPostalCode; 103 104 @Override 105 protected void onCreate(Bundle savedInstanceState) { 106 if (DEBUG) Log.d(TAG, "onCreate"); 107 new AsyncTask<Void, Void, Integer>() { 108 @Override 109 protected Integer doInBackground(Void... arg0) { 110 return TunerHal.getTunerTypeAndCount(TunerSetupActivity.this).first; 111 } 112 113 @Override 114 protected void onPostExecute(Integer result) { 115 if (!TunerSetupActivity.this.isDestroyed()) { 116 mTunerType = result; 117 if (result == null) { 118 finish(); 119 } else { 120 showInitialFragment(); 121 } 122 } 123 } 124 }.execute(); 125 TvApplication.setCurrentRunningProcess(this, false); 126 super.onCreate(savedInstanceState); 127 // TODO: check {@link shouldShowRequestPermissionRationale}. 128 if (checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) 129 != PackageManager.PERMISSION_GRANTED) { 130 // No need to check the request result. 131 requestPermissions(new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION}, 132 PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION); 133 } 134 mTunerHalFactory = new TunerHalFactory(getApplicationContext()); 135 try { 136 // Updating postal code takes time, therefore we called it here for "warm-up". 137 mPreviousPostalCode = PostalCodeUtils.getLastPostalCode(this); 138 PostalCodeUtils.setLastPostalCode(this, null); 139 PostalCodeUtils.updatePostalCode(this); 140 } catch (Exception e) { 141 // Do nothing. If the last known postal code is null, we'll show guided fragment to 142 // prompt users to input postal code before ConnectionTypeFragment is shown. 143 Log.i(TAG, "Can't get postal code:" + e); 144 } 145 } 146 147 @Override 148 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, 149 @NonNull int[] grantResults) { 150 if (requestCode == PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION) { 151 if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED 152 && Experiments.CLOUD_EPG.get()) { 153 try { 154 // Updating postal code takes time, therefore we should update postal code 155 // right after the permission is granted, so that the subsequent operations, 156 // especially EPG fetcher, could get the newly updated postal code. 157 PostalCodeUtils.updatePostalCode(this); 158 } catch (Exception e) { 159 // Do nothing 160 } 161 } 162 } 163 } 164 165 @Override 166 protected Fragment onCreateInitialFragment() { 167 if (mTunerType != null) { 168 SetupFragment fragment = new WelcomeFragment(); 169 Bundle args = new Bundle(); 170 args.putInt(KEY_TUNER_TYPE, mTunerType); 171 fragment.setArguments(args); 172 fragment.setShortDistance(SetupFragment.FRAGMENT_EXIT_TRANSITION 173 | SetupFragment.FRAGMENT_REENTER_TRANSITION); 174 return fragment; 175 } else { 176 return null; 177 } 178 } 179 180 @Override 181 protected boolean executeAction(String category, int actionId, Bundle params) { 182 switch (category) { 183 case WelcomeFragment.ACTION_CATEGORY: 184 switch (actionId) { 185 case SetupMultiPaneFragment.ACTION_DONE: 186 // If the scan was performed, then the result should be OK. 187 setResult(mLastScanFragment == null ? RESULT_CANCELED : RESULT_OK); 188 finish(); 189 break; 190 default: 191 if (mNeedToShowPostalCodeFragment 192 || Features.ENABLE_CLOUD_EPG_REGION.isEnabled( 193 getApplicationContext()) 194 && TextUtils.isEmpty( 195 PostalCodeUtils.getLastPostalCode(this))) { 196 // We cannot get postal code automatically. Postal code input fragment 197 // should always be shown even if users have input some valid postal 198 // code in this activity before. 199 mNeedToShowPostalCodeFragment = true; 200 showPostalCodeFragment(); 201 } else { 202 showConnectionTypeFragment(); 203 } 204 break; 205 } 206 return true; 207 case PostalCodeFragment.ACTION_CATEGORY: 208 if (actionId == SetupMultiPaneFragment.ACTION_DONE 209 || actionId == SetupMultiPaneFragment.ACTION_SKIP) { 210 showConnectionTypeFragment(); 211 } 212 return true; 213 case ConnectionTypeFragment.ACTION_CATEGORY: 214 if (mTunerHalFactory.getOrCreate() == null) { 215 finish(); 216 Toast.makeText(getApplicationContext(), 217 R.string.ut_channel_scan_tuner_unavailable,Toast.LENGTH_LONG).show(); 218 return true; 219 } 220 mLastScanFragment = new ScanFragment(); 221 Bundle args1 = new Bundle(); 222 args1.putInt(ScanFragment.EXTRA_FOR_CHANNEL_SCAN_FILE, 223 CHANNEL_MAP_SCAN_FILE[actionId]); 224 args1.putInt(KEY_TUNER_TYPE, mTunerType); 225 mLastScanFragment.setArguments(args1); 226 showFragment(mLastScanFragment, true); 227 return true; 228 case ScanFragment.ACTION_CATEGORY: 229 switch (actionId) { 230 case ScanFragment.ACTION_CANCEL: 231 getFragmentManager().popBackStack(); 232 return true; 233 case ScanFragment.ACTION_FINISH: 234 mTunerHalFactory.clear(); 235 SetupFragment fragment = new ScanResultFragment(); 236 Bundle args2 = new Bundle(); 237 args2.putInt(KEY_TUNER_TYPE, mTunerType); 238 fragment.setArguments(args2); 239 fragment.setShortDistance(SetupFragment.FRAGMENT_EXIT_TRANSITION 240 | SetupFragment.FRAGMENT_REENTER_TRANSITION); 241 showFragment(fragment, true); 242 return true; 243 } 244 break; 245 case ScanResultFragment.ACTION_CATEGORY: 246 switch (actionId) { 247 case SetupMultiPaneFragment.ACTION_DONE: 248 setResult(RESULT_OK); 249 finish(); 250 break; 251 default: 252 SetupFragment fragment = new ConnectionTypeFragment(); 253 fragment.setShortDistance(SetupFragment.FRAGMENT_ENTER_TRANSITION 254 | SetupFragment.FRAGMENT_RETURN_TRANSITION); 255 showFragment(fragment, true); 256 break; 257 } 258 return true; 259 } 260 return false; 261 } 262 263 @Override 264 public boolean onKeyUp(int keyCode, KeyEvent event) { 265 if (keyCode == KeyEvent.KEYCODE_BACK) { 266 FragmentManager manager = getFragmentManager(); 267 int count = manager.getBackStackEntryCount(); 268 if (count > 0) { 269 String lastTag = manager.getBackStackEntryAt(count - 1).getName(); 270 if (ScanResultFragment.class.getCanonicalName().equals(lastTag) && count >= 2) { 271 // Pops fragment including ScanFragment. 272 manager.popBackStack(manager.getBackStackEntryAt(count - 2).getName(), 273 FragmentManager.POP_BACK_STACK_INCLUSIVE); 274 return true; 275 } else if (ScanFragment.class.getCanonicalName().equals(lastTag)) { 276 mLastScanFragment.finishScan(true); 277 return true; 278 } 279 } 280 } 281 return super.onKeyUp(keyCode, event); 282 } 283 284 @Override 285 public void onDestroy() { 286 if (mPreviousPostalCode != null && PostalCodeUtils.getLastPostalCode(this) == null) { 287 PostalCodeUtils.setLastPostalCode(this, mPreviousPostalCode); 288 } 289 super.onDestroy(); 290 } 291 292 /** 293 * A callback to be invoked when the TvInputService is enabled or disabled. 294 * 295 * @param context a {@link Context} instance 296 * @param enabled {@code true} for the {@link TunerTvInputService} to be enabled; 297 * otherwise {@code false} 298 */ 299 public static void onTvInputEnabled(Context context, boolean enabled, Integer tunerType) { 300 // Send a notification for tuner setup if there's no channels and the tuner TV input 301 // setup has been not done. 302 boolean channelScanDoneOnPreference = TunerPreferences.isScanDone(context); 303 int channelCountOnPreference = TunerPreferences.getScannedChannelCount(context); 304 if (enabled && !channelScanDoneOnPreference && channelCountOnPreference == 0) { 305 TunerPreferences.setShouldShowSetupActivity(context, true); 306 sendNotification(context, tunerType); 307 } else { 308 TunerPreferences.setShouldShowSetupActivity(context, false); 309 cancelNotification(context); 310 } 311 } 312 313 /** 314 * Returns a {@link Intent} to launch the tuner TV input service. 315 * 316 * @param context a {@link Context} instance 317 */ 318 public static Intent createSetupActivity(Context context) { 319 String inputId = TvContract.buildInputId(new ComponentName(context.getPackageName(), 320 TunerTvInputService.class.getName())); 321 322 // Make an intent to launch the setup activity of TV tuner input. 323 Intent intent = TvCommonUtils.createSetupIntent( 324 new Intent(context, TunerSetupActivity.class), inputId); 325 intent.putExtra(TvCommonConstants.EXTRA_INPUT_ID, inputId); 326 Intent tvActivityIntent = new Intent(); 327 tvActivityIntent.setComponent(new ComponentName(context, TV_ACTIVITY_CLASS_NAME)); 328 intent.putExtra(TvCommonConstants.EXTRA_ACTIVITY_AFTER_COMPLETION, tvActivityIntent); 329 return intent; 330 } 331 332 /** 333 * Gets the currently used tuner HAL. 334 */ 335 TunerHal getTunerHal() { 336 return mTunerHalFactory.getOrCreate(); 337 } 338 339 /** 340 * Generates tuner HAL. 341 */ 342 void generateTunerHal() { 343 mTunerHalFactory.generate(); 344 } 345 346 /** 347 * Clears the currently used tuner HAL. 348 */ 349 void clearTunerHal() { 350 mTunerHalFactory.clear(); 351 } 352 353 /** 354 * Returns a {@link PendingIntent} to launch the tuner TV input service. 355 * 356 * @param context a {@link Context} instance 357 */ 358 private static PendingIntent createPendingIntentForSetupActivity(Context context) { 359 return PendingIntent.getActivity(context, 0, createSetupActivity(context), 360 PendingIntent.FLAG_UPDATE_CURRENT); 361 } 362 363 private static void sendNotification(Context context, Integer tunerType) { 364 SoftPreconditions.checkState(tunerType != null, TAG, 365 "tunerType is null when send notification"); 366 if (tunerType == null) { 367 return; 368 } 369 Resources resources = context.getResources(); 370 String contentTitle = resources.getString(R.string.ut_setup_notification_content_title); 371 int contentTextId = 0; 372 switch (tunerType) { 373 case TunerHal.TUNER_TYPE_BUILT_IN: 374 contentTextId = R.string.bt_setup_notification_content_text; 375 break; 376 case TunerHal.TUNER_TYPE_USB: 377 contentTextId = R.string.ut_setup_notification_content_text; 378 break; 379 case TunerHal.TUNER_TYPE_NETWORK: 380 contentTextId = R.string.nt_setup_notification_content_text; 381 break; 382 } 383 String contentText = resources.getString(contentTextId); 384 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 385 sendNotificationInternal(context, contentTitle, contentText); 386 } else { 387 Bitmap largeIcon = BitmapFactory.decodeResource(resources, 388 R.drawable.recommendation_antenna); 389 sendRecommendationCard(context, contentTitle, contentText, largeIcon); 390 } 391 } 392 393 /** 394 * Sends the recommendation card to start the tuner TV input setup activity. 395 * 396 * @param context a {@link Context} instance 397 */ 398 private static void sendRecommendationCard(Context context, String contentTitle, 399 String contentText, Bitmap largeIcon) { 400 // Build and send the notification. 401 Notification notification = new NotificationCompat.BigPictureStyle( 402 new NotificationCompat.Builder(context) 403 .setAutoCancel(false) 404 .setContentTitle(contentTitle) 405 .setContentText(contentText) 406 .setContentInfo(contentText) 407 .setCategory(Notification.CATEGORY_RECOMMENDATION) 408 .setLargeIcon(largeIcon) 409 .setSmallIcon(context.getResources().getIdentifier( 410 TAG_ICON, TAG_DRAWABLE, context.getPackageName())) 411 .setContentIntent(createPendingIntentForSetupActivity(context))) 412 .build(); 413 NotificationManager notificationManager = (NotificationManager) context 414 .getSystemService(Context.NOTIFICATION_SERVICE); 415 notificationManager.notify(NOTIFY_TAG, NOTIFY_ID, notification); 416 } 417 418 private static void sendNotificationInternal(Context context, String contentTitle, 419 String contentText) { 420 NotificationManager notificationManager = (NotificationManager) context.getSystemService( 421 Context.NOTIFICATION_SERVICE); 422 notificationManager.createNotificationChannel(new NotificationChannel( 423 TUNER_SET_UP_NOTIFICATION_CHANNEL_ID, 424 context.getResources().getString(R.string.ut_setup_notification_channel_name), 425 NotificationManager.IMPORTANCE_HIGH)); 426 Notification notification = new Notification.Builder( 427 context, TUNER_SET_UP_NOTIFICATION_CHANNEL_ID) 428 .setContentTitle(contentTitle) 429 .setContentText(contentText) 430 .setSmallIcon(context.getResources().getIdentifier( 431 TAG_ICON, TAG_DRAWABLE, context.getPackageName())) 432 .setContentIntent(createPendingIntentForSetupActivity(context)) 433 .setVisibility(Notification.VISIBILITY_PUBLIC) 434 .extend(new Notification.TvExtender()) 435 .build(); 436 notificationManager.notify(NOTIFY_TAG, NOTIFY_ID, notification); 437 } 438 439 private void showPostalCodeFragment() { 440 SetupFragment fragment = new PostalCodeFragment(); 441 fragment.setShortDistance(SetupFragment.FRAGMENT_ENTER_TRANSITION 442 | SetupFragment.FRAGMENT_RETURN_TRANSITION); 443 showFragment(fragment, true); 444 } 445 446 private void showConnectionTypeFragment() { 447 SetupFragment fragment = new ConnectionTypeFragment(); 448 fragment.setShortDistance(SetupFragment.FRAGMENT_ENTER_TRANSITION 449 | SetupFragment.FRAGMENT_RETURN_TRANSITION); 450 showFragment(fragment, true); 451 } 452 453 /** 454 * Cancels the previously shown notification. 455 * 456 * @param context a {@link Context} instance 457 */ 458 public static void cancelNotification(Context context) { 459 NotificationManager notificationManager = (NotificationManager) context 460 .getSystemService(Context.NOTIFICATION_SERVICE); 461 notificationManager.cancel(NOTIFY_TAG, NOTIFY_ID); 462 } 463 464 @VisibleForTesting 465 static class TunerHalFactory { 466 private Context mContext; 467 @VisibleForTesting 468 TunerHal mTunerHal; 469 private GenerateTunerHalTask mGenerateTunerHalTask; 470 private final Executor mExecutor; 471 472 TunerHalFactory(Context context) { 473 this(context, AsyncTask.SERIAL_EXECUTOR); 474 } 475 476 TunerHalFactory(Context context, Executor executor) { 477 mContext = context; 478 mExecutor = executor; 479 } 480 481 /** 482 * Returns tuner HAL currently used. If it's {@code null} and tuner HAL is not generated 483 * before, tries to generate it synchronously. 484 */ 485 @WorkerThread 486 TunerHal getOrCreate() { 487 if (mGenerateTunerHalTask != null 488 && mGenerateTunerHalTask.getStatus() != AsyncTask.Status.FINISHED) { 489 try { 490 return mGenerateTunerHalTask.get(); 491 } catch (Exception e) { 492 Log.e(TAG, "Cannot get Tuner HAL: " + e); 493 } 494 } else if (mGenerateTunerHalTask == null && mTunerHal == null) { 495 mTunerHal = createInstance(); 496 } 497 return mTunerHal; 498 } 499 500 /** 501 * Generates tuner hal for scanning with asynchronous tasks. 502 */ 503 @MainThread 504 void generate() { 505 if (mGenerateTunerHalTask == null && mTunerHal == null) { 506 mGenerateTunerHalTask = new GenerateTunerHalTask(); 507 mGenerateTunerHalTask.executeOnExecutor(mExecutor); 508 } 509 } 510 511 /** 512 * Clears the currently used tuner hal. 513 */ 514 @MainThread 515 void clear() { 516 if (mGenerateTunerHalTask != null) { 517 mGenerateTunerHalTask.cancel(true); 518 mGenerateTunerHalTask = null; 519 } 520 if (mTunerHal != null) { 521 AutoCloseableUtils.closeQuietly(mTunerHal); 522 mTunerHal = null; 523 } 524 } 525 526 @WorkerThread 527 protected TunerHal createInstance() { 528 return TunerHal.createInstance(mContext); 529 } 530 531 class GenerateTunerHalTask extends AsyncTask<Void, Void, TunerHal> { 532 @Override 533 protected TunerHal doInBackground(Void... args) { 534 return createInstance(); 535 } 536 537 @Override 538 protected void onPostExecute(TunerHal tunerHal) { 539 mTunerHal = tunerHal; 540 } 541 } 542 } 543 }