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