/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tv.tuner.setup; import android.app.Fragment; import android.app.FragmentManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.tv.TvContract; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import android.support.v4.app.NotificationCompat; import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; import android.widget.Toast; import com.android.tv.Features; import com.android.tv.TvApplication; import com.android.tv.common.AutoCloseableUtils; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.TvCommonConstants; import com.android.tv.common.TvCommonUtils; import com.android.tv.common.ui.setup.SetupActivity; import com.android.tv.common.ui.setup.SetupFragment; import com.android.tv.common.ui.setup.SetupMultiPaneFragment; import com.android.tv.experiments.Experiments; import com.android.tv.tuner.R; import com.android.tv.tuner.TunerHal; import com.android.tv.tuner.TunerPreferences; import com.android.tv.tuner.tvinput.TunerTvInputService; import com.android.tv.tuner.util.PostalCodeUtils; import java.util.concurrent.Executor; /** * An activity that serves tuner setup process. */ public class TunerSetupActivity extends SetupActivity { private static final String TAG = "TunerSetupActivity"; private static final boolean DEBUG = false; /** * Key for passing tuner type to sub-fragments. */ public static final String KEY_TUNER_TYPE = "TunerSetupActivity.tunerType"; // For the notification. private static final String TV_ACTIVITY_CLASS_NAME = "com.android.tv.TvActivity"; private static final String TUNER_SET_UP_NOTIFICATION_CHANNEL_ID = "tuner_setup_channel"; private static final String NOTIFY_TAG = "TunerSetup"; private static final int NOTIFY_ID = 1000; private static final String TAG_DRAWABLE = "drawable"; private static final String TAG_ICON = "ic_launcher_s"; private static final int PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION = 1; private static final int CHANNEL_MAP_SCAN_FILE[] = { R.raw.ut_us_atsc_center_frequencies_8vsb, R.raw.ut_us_cable_standard_center_frequencies_qam256, R.raw.ut_us_all, R.raw.ut_kr_atsc_center_frequencies_8vsb, R.raw.ut_kr_cable_standard_center_frequencies_qam256, R.raw.ut_kr_all, R.raw.ut_kr_dev_cj_cable_center_frequencies_qam256, R.raw.ut_euro_dvbt_all, R.raw.ut_euro_dvbt_all, R.raw.ut_euro_dvbt_all }; private ScanFragment mLastScanFragment; private Integer mTunerType; private TunerHalFactory mTunerHalFactory; private boolean mNeedToShowPostalCodeFragment; private String mPreviousPostalCode; @Override protected void onCreate(Bundle savedInstanceState) { if (DEBUG) Log.d(TAG, "onCreate"); new AsyncTask() { @Override protected Integer doInBackground(Void... arg0) { return TunerHal.getTunerTypeAndCount(TunerSetupActivity.this).first; } @Override protected void onPostExecute(Integer result) { if (!TunerSetupActivity.this.isDestroyed()) { mTunerType = result; if (result == null) { finish(); } else { showInitialFragment(); } } } }.execute(); TvApplication.setCurrentRunningProcess(this, false); super.onCreate(savedInstanceState); // TODO: check {@link shouldShowRequestPermissionRationale}. if (checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { // No need to check the request result. requestPermissions(new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION}, PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION); } mTunerHalFactory = new TunerHalFactory(getApplicationContext()); try { // Updating postal code takes time, therefore we called it here for "warm-up". mPreviousPostalCode = PostalCodeUtils.getLastPostalCode(this); PostalCodeUtils.setLastPostalCode(this, null); PostalCodeUtils.updatePostalCode(this); } catch (Exception e) { // Do nothing. If the last known postal code is null, we'll show guided fragment to // prompt users to input postal code before ConnectionTypeFragment is shown. Log.i(TAG, "Can't get postal code:" + e); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED && Experiments.CLOUD_EPG.get()) { try { // Updating postal code takes time, therefore we should update postal code // right after the permission is granted, so that the subsequent operations, // especially EPG fetcher, could get the newly updated postal code. PostalCodeUtils.updatePostalCode(this); } catch (Exception e) { // Do nothing } } } } @Override protected Fragment onCreateInitialFragment() { if (mTunerType != null) { SetupFragment fragment = new WelcomeFragment(); Bundle args = new Bundle(); args.putInt(KEY_TUNER_TYPE, mTunerType); fragment.setArguments(args); fragment.setShortDistance(SetupFragment.FRAGMENT_EXIT_TRANSITION | SetupFragment.FRAGMENT_REENTER_TRANSITION); return fragment; } else { return null; } } @Override protected boolean executeAction(String category, int actionId, Bundle params) { switch (category) { case WelcomeFragment.ACTION_CATEGORY: switch (actionId) { case SetupMultiPaneFragment.ACTION_DONE: // If the scan was performed, then the result should be OK. setResult(mLastScanFragment == null ? RESULT_CANCELED : RESULT_OK); finish(); break; default: if (mNeedToShowPostalCodeFragment || Features.ENABLE_CLOUD_EPG_REGION.isEnabled( getApplicationContext()) && TextUtils.isEmpty( PostalCodeUtils.getLastPostalCode(this))) { // We cannot get postal code automatically. Postal code input fragment // should always be shown even if users have input some valid postal // code in this activity before. mNeedToShowPostalCodeFragment = true; showPostalCodeFragment(); } else { showConnectionTypeFragment(); } break; } return true; case PostalCodeFragment.ACTION_CATEGORY: if (actionId == SetupMultiPaneFragment.ACTION_DONE || actionId == SetupMultiPaneFragment.ACTION_SKIP) { showConnectionTypeFragment(); } return true; case ConnectionTypeFragment.ACTION_CATEGORY: if (mTunerHalFactory.getOrCreate() == null) { finish(); Toast.makeText(getApplicationContext(), R.string.ut_channel_scan_tuner_unavailable,Toast.LENGTH_LONG).show(); return true; } mLastScanFragment = new ScanFragment(); Bundle args1 = new Bundle(); args1.putInt(ScanFragment.EXTRA_FOR_CHANNEL_SCAN_FILE, CHANNEL_MAP_SCAN_FILE[actionId]); args1.putInt(KEY_TUNER_TYPE, mTunerType); mLastScanFragment.setArguments(args1); showFragment(mLastScanFragment, true); return true; case ScanFragment.ACTION_CATEGORY: switch (actionId) { case ScanFragment.ACTION_CANCEL: getFragmentManager().popBackStack(); return true; case ScanFragment.ACTION_FINISH: mTunerHalFactory.clear(); SetupFragment fragment = new ScanResultFragment(); Bundle args2 = new Bundle(); args2.putInt(KEY_TUNER_TYPE, mTunerType); fragment.setArguments(args2); fragment.setShortDistance(SetupFragment.FRAGMENT_EXIT_TRANSITION | SetupFragment.FRAGMENT_REENTER_TRANSITION); showFragment(fragment, true); return true; } break; case ScanResultFragment.ACTION_CATEGORY: switch (actionId) { case SetupMultiPaneFragment.ACTION_DONE: setResult(RESULT_OK); finish(); break; default: SetupFragment fragment = new ConnectionTypeFragment(); fragment.setShortDistance(SetupFragment.FRAGMENT_ENTER_TRANSITION | SetupFragment.FRAGMENT_RETURN_TRANSITION); showFragment(fragment, true); break; } return true; } return false; } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { FragmentManager manager = getFragmentManager(); int count = manager.getBackStackEntryCount(); if (count > 0) { String lastTag = manager.getBackStackEntryAt(count - 1).getName(); if (ScanResultFragment.class.getCanonicalName().equals(lastTag) && count >= 2) { // Pops fragment including ScanFragment. manager.popBackStack(manager.getBackStackEntryAt(count - 2).getName(), FragmentManager.POP_BACK_STACK_INCLUSIVE); return true; } else if (ScanFragment.class.getCanonicalName().equals(lastTag)) { mLastScanFragment.finishScan(true); return true; } } } return super.onKeyUp(keyCode, event); } @Override public void onDestroy() { if (mPreviousPostalCode != null && PostalCodeUtils.getLastPostalCode(this) == null) { PostalCodeUtils.setLastPostalCode(this, mPreviousPostalCode); } super.onDestroy(); } /** * A callback to be invoked when the TvInputService is enabled or disabled. * * @param context a {@link Context} instance * @param enabled {@code true} for the {@link TunerTvInputService} to be enabled; * otherwise {@code false} */ public static void onTvInputEnabled(Context context, boolean enabled, Integer tunerType) { // Send a notification for tuner setup if there's no channels and the tuner TV input // setup has been not done. boolean channelScanDoneOnPreference = TunerPreferences.isScanDone(context); int channelCountOnPreference = TunerPreferences.getScannedChannelCount(context); if (enabled && !channelScanDoneOnPreference && channelCountOnPreference == 0) { TunerPreferences.setShouldShowSetupActivity(context, true); sendNotification(context, tunerType); } else { TunerPreferences.setShouldShowSetupActivity(context, false); cancelNotification(context); } } /** * Returns a {@link Intent} to launch the tuner TV input service. * * @param context a {@link Context} instance */ public static Intent createSetupActivity(Context context) { String inputId = TvContract.buildInputId(new ComponentName(context.getPackageName(), TunerTvInputService.class.getName())); // Make an intent to launch the setup activity of TV tuner input. Intent intent = TvCommonUtils.createSetupIntent( new Intent(context, TunerSetupActivity.class), inputId); intent.putExtra(TvCommonConstants.EXTRA_INPUT_ID, inputId); Intent tvActivityIntent = new Intent(); tvActivityIntent.setComponent(new ComponentName(context, TV_ACTIVITY_CLASS_NAME)); intent.putExtra(TvCommonConstants.EXTRA_ACTIVITY_AFTER_COMPLETION, tvActivityIntent); return intent; } /** * Gets the currently used tuner HAL. */ TunerHal getTunerHal() { return mTunerHalFactory.getOrCreate(); } /** * Generates tuner HAL. */ void generateTunerHal() { mTunerHalFactory.generate(); } /** * Clears the currently used tuner HAL. */ void clearTunerHal() { mTunerHalFactory.clear(); } /** * Returns a {@link PendingIntent} to launch the tuner TV input service. * * @param context a {@link Context} instance */ private static PendingIntent createPendingIntentForSetupActivity(Context context) { return PendingIntent.getActivity(context, 0, createSetupActivity(context), PendingIntent.FLAG_UPDATE_CURRENT); } private static void sendNotification(Context context, Integer tunerType) { SoftPreconditions.checkState(tunerType != null, TAG, "tunerType is null when send notification"); if (tunerType == null) { return; } Resources resources = context.getResources(); String contentTitle = resources.getString(R.string.ut_setup_notification_content_title); int contentTextId = 0; switch (tunerType) { case TunerHal.TUNER_TYPE_BUILT_IN: contentTextId = R.string.bt_setup_notification_content_text; break; case TunerHal.TUNER_TYPE_USB: contentTextId = R.string.ut_setup_notification_content_text; break; case TunerHal.TUNER_TYPE_NETWORK: contentTextId = R.string.nt_setup_notification_content_text; break; } String contentText = resources.getString(contentTextId); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { sendNotificationInternal(context, contentTitle, contentText); } else { Bitmap largeIcon = BitmapFactory.decodeResource(resources, R.drawable.recommendation_antenna); sendRecommendationCard(context, contentTitle, contentText, largeIcon); } } /** * Sends the recommendation card to start the tuner TV input setup activity. * * @param context a {@link Context} instance */ private static void sendRecommendationCard(Context context, String contentTitle, String contentText, Bitmap largeIcon) { // Build and send the notification. Notification notification = new NotificationCompat.BigPictureStyle( new NotificationCompat.Builder(context) .setAutoCancel(false) .setContentTitle(contentTitle) .setContentText(contentText) .setContentInfo(contentText) .setCategory(Notification.CATEGORY_RECOMMENDATION) .setLargeIcon(largeIcon) .setSmallIcon(context.getResources().getIdentifier( TAG_ICON, TAG_DRAWABLE, context.getPackageName())) .setContentIntent(createPendingIntentForSetupActivity(context))) .build(); NotificationManager notificationManager = (NotificationManager) context .getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(NOTIFY_TAG, NOTIFY_ID, notification); } private static void sendNotificationInternal(Context context, String contentTitle, String contentText) { NotificationManager notificationManager = (NotificationManager) context.getSystemService( Context.NOTIFICATION_SERVICE); notificationManager.createNotificationChannel(new NotificationChannel( TUNER_SET_UP_NOTIFICATION_CHANNEL_ID, context.getResources().getString(R.string.ut_setup_notification_channel_name), NotificationManager.IMPORTANCE_HIGH)); Notification notification = new Notification.Builder( context, TUNER_SET_UP_NOTIFICATION_CHANNEL_ID) .setContentTitle(contentTitle) .setContentText(contentText) .setSmallIcon(context.getResources().getIdentifier( TAG_ICON, TAG_DRAWABLE, context.getPackageName())) .setContentIntent(createPendingIntentForSetupActivity(context)) .setVisibility(Notification.VISIBILITY_PUBLIC) .extend(new Notification.TvExtender()) .build(); notificationManager.notify(NOTIFY_TAG, NOTIFY_ID, notification); } private void showPostalCodeFragment() { SetupFragment fragment = new PostalCodeFragment(); fragment.setShortDistance(SetupFragment.FRAGMENT_ENTER_TRANSITION | SetupFragment.FRAGMENT_RETURN_TRANSITION); showFragment(fragment, true); } private void showConnectionTypeFragment() { SetupFragment fragment = new ConnectionTypeFragment(); fragment.setShortDistance(SetupFragment.FRAGMENT_ENTER_TRANSITION | SetupFragment.FRAGMENT_RETURN_TRANSITION); showFragment(fragment, true); } /** * Cancels the previously shown notification. * * @param context a {@link Context} instance */ public static void cancelNotification(Context context) { NotificationManager notificationManager = (NotificationManager) context .getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.cancel(NOTIFY_TAG, NOTIFY_ID); } @VisibleForTesting static class TunerHalFactory { private Context mContext; @VisibleForTesting TunerHal mTunerHal; private GenerateTunerHalTask mGenerateTunerHalTask; private final Executor mExecutor; TunerHalFactory(Context context) { this(context, AsyncTask.SERIAL_EXECUTOR); } TunerHalFactory(Context context, Executor executor) { mContext = context; mExecutor = executor; } /** * Returns tuner HAL currently used. If it's {@code null} and tuner HAL is not generated * before, tries to generate it synchronously. */ @WorkerThread TunerHal getOrCreate() { if (mGenerateTunerHalTask != null && mGenerateTunerHalTask.getStatus() != AsyncTask.Status.FINISHED) { try { return mGenerateTunerHalTask.get(); } catch (Exception e) { Log.e(TAG, "Cannot get Tuner HAL: " + e); } } else if (mGenerateTunerHalTask == null && mTunerHal == null) { mTunerHal = createInstance(); } return mTunerHal; } /** * Generates tuner hal for scanning with asynchronous tasks. */ @MainThread void generate() { if (mGenerateTunerHalTask == null && mTunerHal == null) { mGenerateTunerHalTask = new GenerateTunerHalTask(); mGenerateTunerHalTask.executeOnExecutor(mExecutor); } } /** * Clears the currently used tuner hal. */ @MainThread void clear() { if (mGenerateTunerHalTask != null) { mGenerateTunerHalTask.cancel(true); mGenerateTunerHalTask = null; } if (mTunerHal != null) { AutoCloseableUtils.closeQuietly(mTunerHal); mTunerHal = null; } } @WorkerThread protected TunerHal createInstance() { return TunerHal.createInstance(mContext); } class GenerateTunerHalTask extends AsyncTask { @Override protected TunerHal doInBackground(Void... args) { return createInstance(); } @Override protected void onPostExecute(TunerHal tunerHal) { mTunerHal = tunerHal; } } } }