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
17package com.android.tv.tuner.setup;
18
19import android.app.Fragment;
20import android.app.FragmentManager;
21import android.app.Notification;
22import android.app.NotificationChannel;
23import android.app.NotificationManager;
24import android.app.PendingIntent;
25import android.content.ComponentName;
26import android.content.Context;
27import android.content.Intent;
28import android.content.pm.PackageManager;
29import android.content.res.Resources;
30import android.graphics.Bitmap;
31import android.graphics.BitmapFactory;
32import android.media.tv.TvContract;
33import android.os.AsyncTask;
34import android.os.Build;
35import android.os.Bundle;
36import android.support.annotation.MainThread;
37import android.support.annotation.NonNull;
38import android.support.annotation.VisibleForTesting;
39import android.support.annotation.WorkerThread;
40import android.support.v4.app.NotificationCompat;
41import android.text.TextUtils;
42import android.util.Log;
43import android.view.KeyEvent;
44import android.widget.Toast;
45
46import com.android.tv.Features;
47import com.android.tv.TvApplication;
48import com.android.tv.common.AutoCloseableUtils;
49import com.android.tv.common.SoftPreconditions;
50import com.android.tv.common.TvCommonConstants;
51import com.android.tv.common.TvCommonUtils;
52import com.android.tv.common.ui.setup.SetupActivity;
53import com.android.tv.common.ui.setup.SetupFragment;
54import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
55import com.android.tv.experiments.Experiments;
56import com.android.tv.tuner.R;
57import com.android.tv.tuner.TunerHal;
58import com.android.tv.tuner.TunerPreferences;
59import com.android.tv.tuner.tvinput.TunerTvInputService;
60import com.android.tv.tuner.util.PostalCodeUtils;
61
62import java.util.concurrent.Executor;
63
64/**
65 * An activity that serves tuner setup process.
66 */
67public 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}