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