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