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