• 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.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
onCreate(Bundle savedInstanceState)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 
executeGetTunerTypeAndCountAsyncTask()122     protected void executeGetTunerTypeAndCountAsyncTask() {}
123 
124     @Override
onStop()125     protected void onStop() {
126         mActivityStopped = true;
127         super.onStop();
128     }
129 
130     @Override
onResume()131     protected void onResume() {
132         super.onResume();
133         mActivityStopped = false;
134         if (mPendingShowInitialFragment) {
135             showInitialFragment();
136             mPendingShowInitialFragment = false;
137         }
138     }
139 
140     @Override
onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)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
onCreateInitialFragment()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
executeAction(String category, int actionId, Bundle params)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
onDestroy()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. */
getTunerHal()272     TunerHal getTunerHal() {
273         return mTunerHalFactory.getOrCreate();
274     }
275 
276     /** Generates tuner HAL. */
generateTunerHal()277     void generateTunerHal() {
278         mTunerHalFactory.generate();
279     }
280 
281     /** Clears the currently used tuner HAL. */
clearTunerHal()282     protected void clearTunerHal() {
283         mTunerHalFactory.clear();
284     }
285 
showPostalCodeFragment()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 
showConnectionTypeFragment()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 
showScanResultFragment()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      */
cancelNotification(Context context)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      */
onTvInputEnabled(Context context, boolean enabled, Integer tunerType)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 
sendNotification(Context context, Integer tunerType)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 
sendNotificationInternal( Context context, String contentTitle, String contentText)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      */
sendRecommendationCard( Context context, String contentTitle, String contentText, Bitmap largeIcon)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      */
createPendingIntentForSetupActivity(Context context)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 
TunerHalFactory(Context context)449         TunerHalFactory(Context context) {
450             this(context, AsyncTask.SERIAL_EXECUTOR);
451         }
452 
TunerHalFactory(Context context, Executor executor)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
getOrCreate()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
generate()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
clear()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
createInstance()500         protected TunerHal createInstance() {
501             return TunerHal.createInstance(mContext);
502         }
503 
504         class GenerateTunerHalTask extends AsyncTask<Void, Void, TunerHal> {
505             @Override
doInBackground(Void... args)506             protected TunerHal doInBackground(Void... args) {
507                 return createInstance();
508             }
509 
510             @Override
onPostExecute(TunerHal tunerHal)511             protected void onPostExecute(TunerHal tunerHal) {
512                 mTunerHal = tunerHal;
513             }
514         }
515     }
516 }
517