Home | History | Annotate | Download | only in setup
      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