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