com.android.tv.tuner.setup.TunerSetupActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tv.tuner.setup.TunerSetupActivity.java

Source

/*
 * 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<Void, Void, Integer>() {
            @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<Void, Void, TunerHal> {
            @Override
            protected TunerHal doInBackground(Void... args) {
                return createInstance();
            }

            @Override
            protected void onPostExecute(TunerHal tunerHal) {
                mTunerHal = tunerHal;
            }
        }
    }
}