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