Home | History | Annotate | Download | only in onboarding
      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.onboarding;
     18 
     19 import android.content.ActivityNotFoundException;
     20 import android.content.ComponentName;
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.graphics.Typeface;
     24 import android.graphics.drawable.Drawable;
     25 import android.media.tv.TvInputInfo;
     26 import android.media.tv.TvInputManager.TvInputCallback;
     27 import android.os.Bundle;
     28 import android.support.annotation.NonNull;
     29 import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
     30 import android.support.v17.leanback.widget.GuidedAction;
     31 import android.support.v17.leanback.widget.GuidedActionsStylist;
     32 import android.support.v17.leanback.widget.VerticalGridView;
     33 import android.view.LayoutInflater;
     34 import android.view.View;
     35 import android.view.ViewGroup;
     36 import android.widget.ImageView;
     37 import android.widget.TextView;
     38 import android.widget.Toast;
     39 
     40 import com.android.tv.ApplicationSingletons;
     41 import com.android.tv.Features;
     42 import com.android.tv.R;
     43 import com.android.tv.SetupPassthroughActivity;
     44 import com.android.tv.TvApplication;
     45 import com.android.tv.common.TvCommonUtils;
     46 import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
     47 import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
     48 import com.android.tv.data.ChannelDataManager;
     49 import com.android.tv.data.TvInputNewComparator;
     50 import com.android.tv.util.SetupUtils;
     51 import com.android.tv.util.TvInputManagerHelper;
     52 import com.android.tv.util.Utils;
     53 
     54 import java.util.ArrayList;
     55 import java.util.Collections;
     56 import java.util.List;
     57 
     58 /**
     59  * A fragment for channel source info/setup.
     60  */
     61 public class SetupSourcesFragment extends SetupMultiPaneFragment {
     62     private static final String TAG = "SetupSourcesFragment";
     63 
     64     public static final String ACTION_CATEGORY =
     65             "com.android.tv.onboarding.SetupSourcesFragment";
     66     public static final int ACTION_PLAY_STORE = 1;
     67 
     68     private static final String SETUP_TRACKER_LABEL = "Setup fragment";
     69 
     70     private InputSetupRunnable mInputSetupRunnable;
     71 
     72     private ContentFragment mContentFragment;
     73 
     74     @Override
     75     public View onCreateView(LayoutInflater inflater, ViewGroup container,
     76             Bundle savedInstanceState) {
     77         View view = super.onCreateView(inflater, container, savedInstanceState);
     78         TvApplication.getSingletons(getActivity()).getTracker().sendScreenView(SETUP_TRACKER_LABEL);
     79         return view;
     80     }
     81 
     82     @Override
     83     protected void onEnterTransitionEnd() {
     84         if (mContentFragment != null) {
     85             mContentFragment.executePendingAction();
     86         }
     87     }
     88 
     89     @Override
     90     protected SetupGuidedStepFragment onCreateContentFragment() {
     91         mContentFragment = new ContentFragment();
     92         mContentFragment.setParentFragment(this);
     93         Bundle arguments = new Bundle();
     94         arguments.putBoolean(SetupGuidedStepFragment.KEY_THREE_PANE, true);
     95         mContentFragment.setArguments(arguments);
     96         return mContentFragment;
     97     }
     98 
     99     @Override
    100     protected String getActionCategory() {
    101         return ACTION_CATEGORY;
    102     }
    103 
    104     /**
    105      * Call this method to run customized input setup.
    106      *
    107      * @param runnable runnable to be called when the input setup is necessary.
    108      */
    109     public void setInputSetupRunnable(InputSetupRunnable runnable) {
    110         mInputSetupRunnable = runnable;
    111     }
    112 
    113     /**
    114      * Interface for the customized input setup.
    115      */
    116     public interface InputSetupRunnable {
    117         /**
    118          * Called for the input setup.
    119          *
    120          * @param input TV input for setup.
    121          */
    122         void runInputSetup(TvInputInfo input);
    123     }
    124 
    125     public static class ContentFragment extends SetupGuidedStepFragment {
    126         private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1;
    127 
    128         // ACTION_PLAY_STORE is defined in the outer class.
    129         private static final int ACTION_DIVIDER = 2;
    130         private static final int ACTION_HEADER = 3;
    131         private static final int ACTION_INPUT_START = 4;
    132 
    133         private static final int PENDING_ACTION_NONE = 0;
    134         private static final int PENDING_ACTION_INPUT_CHANGED = 1;
    135         private static final int PENDING_ACTION_CHANNEL_CHANGED = 2;
    136 
    137         private TvInputManagerHelper mInputManager;
    138         private ChannelDataManager mChannelDataManager;
    139         private SetupUtils mSetupUtils;
    140         private List<TvInputInfo> mInputs;
    141         private int mKnownInputStartIndex;
    142         private int mDoneInputStartIndex;
    143 
    144         private SetupSourcesFragment mParentFragment;
    145 
    146         private String mNewlyAddedInputId;
    147 
    148         private int mPendingAction = PENDING_ACTION_NONE;
    149 
    150         private final TvInputCallback mInputCallback = new TvInputCallback() {
    151             @Override
    152             public void onInputAdded(String inputId) {
    153                 handleInputChanged();
    154             }
    155 
    156             @Override
    157             public void onInputRemoved(String inputId) {
    158                 handleInputChanged();
    159             }
    160 
    161             @Override
    162             public void onInputUpdated(String inputId) {
    163                 handleInputChanged();
    164             }
    165 
    166             private void handleInputChanged() {
    167                 // The actions created while enter transition is running will not be included in the
    168                 // fragment transition.
    169                 if (mParentFragment.isEnterTransitionRunning()) {
    170                     mPendingAction = PENDING_ACTION_INPUT_CHANGED;
    171                     return;
    172                 }
    173                 buildInputs();
    174                 updateActions();
    175             }
    176         };
    177 
    178         void setParentFragment(SetupSourcesFragment parentFragment) {
    179             mParentFragment = parentFragment;
    180         }
    181 
    182         private final ChannelDataManager.Listener mChannelDataManagerListener
    183                 = new ChannelDataManager.Listener() {
    184             @Override
    185             public void onLoadFinished() {
    186                 handleChannelChanged();
    187             }
    188 
    189             @Override
    190             public void onChannelListUpdated() {
    191                 handleChannelChanged();
    192             }
    193 
    194             @Override
    195             public void onChannelBrowsableChanged() {
    196                 handleChannelChanged();
    197             }
    198 
    199             private void handleChannelChanged() {
    200                 // The actions created while enter transition is running will not be included in the
    201                 // fragment transition.
    202                 if (mParentFragment.isEnterTransitionRunning()) {
    203                     if (mPendingAction != PENDING_ACTION_INPUT_CHANGED) {
    204                         mPendingAction = PENDING_ACTION_CHANNEL_CHANGED;
    205                     }
    206                     return;
    207                 }
    208                 updateActions();
    209             }
    210         };
    211 
    212         @Override
    213         public void onCreate(Bundle savedInstanceState) {
    214             // TODO: Handle USB TV tuner differently.
    215             Context context = getActivity();
    216             ApplicationSingletons app = TvApplication.getSingletons(context);
    217             mInputManager = app.getTvInputManagerHelper();
    218             mChannelDataManager = app.getChannelDataManager();
    219             mSetupUtils = SetupUtils.getInstance(context);
    220             buildInputs();
    221             mInputManager.addCallback(mInputCallback);
    222             mChannelDataManager.addListener(mChannelDataManagerListener);
    223             super.onCreate(savedInstanceState);
    224         }
    225 
    226         @Override
    227         public void onDestroy() {
    228             super.onDestroy();
    229             mChannelDataManager.removeListener(mChannelDataManagerListener);
    230             mInputManager.removeCallback(mInputCallback);
    231         }
    232 
    233         @NonNull
    234         @Override
    235         public Guidance onCreateGuidance(Bundle savedInstanceState) {
    236             String title = getString(R.string.setup_sources_text);
    237             String description = getString(R.string.setup_sources_description);
    238             return new Guidance(title, description, null, null);
    239         }
    240 
    241         @Override
    242         public GuidedActionsStylist onCreateActionsStylist() {
    243             return new SetupSourceGuidedActionsStylist();
    244         }
    245 
    246         @Override
    247         public void onCreateActions(@NonNull List<GuidedAction> actions,
    248                 Bundle savedInstanceState) {
    249             createActionsInternal(actions);
    250         }
    251 
    252         private void buildInputs() {
    253             List<TvInputInfo> oldInputs = mInputs;
    254             mInputs = mInputManager.getTvInputInfos(true, true);
    255             // Get newly installed input ID.
    256             if (oldInputs != null) {
    257                 List<TvInputInfo> newList = new ArrayList<>(mInputs);
    258                 for (TvInputInfo input : oldInputs) {
    259                     newList.remove(input);
    260                 }
    261                 if (newList.size() > 0 && mSetupUtils.isNewInput(newList.get(0).getId())) {
    262                     mNewlyAddedInputId = newList.get(0).getId();
    263                 } else {
    264                     mNewlyAddedInputId = null;
    265                 }
    266             }
    267             Collections.sort(mInputs, new TvInputNewComparator(mSetupUtils, mInputManager));
    268             mKnownInputStartIndex = 0;
    269             mDoneInputStartIndex = 0;
    270             for (TvInputInfo input : mInputs) {
    271                 if (mSetupUtils.isNewInput(input.getId())) {
    272                     mSetupUtils.markAsKnownInput(input.getId());
    273                     ++mKnownInputStartIndex;
    274                 }
    275                 if (!mSetupUtils.isSetupDone(input.getId())) {
    276                     ++mDoneInputStartIndex;
    277                 }
    278             }
    279         }
    280 
    281         private void updateActions() {
    282             List<GuidedAction> actions = new ArrayList<>();
    283             createActionsInternal(actions);
    284             setActions(actions);
    285         }
    286 
    287         private void createActionsInternal(List<GuidedAction> actions) {
    288             int newPosition = -1;
    289             int position = 0;
    290             if (mDoneInputStartIndex > 0) {
    291                 // Need a "New" category
    292                 actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_HEADER)
    293                         .title(null).description(getString(R.string.setup_category_new))
    294                         .focusable(false).build());
    295             }
    296             for (int i = 0; i < mInputs.size(); ++i) {
    297                 if (i == mDoneInputStartIndex) {
    298                     ++position;
    299                     actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_HEADER)
    300                             .title(null).description(getString(R.string.setup_category_done))
    301                             .focusable(false).build());
    302                 }
    303                 TvInputInfo input = mInputs.get(i);
    304                 String inputId = input.getId();
    305                 String description;
    306                 int channelCount = mChannelDataManager.getChannelCountForInput(inputId);
    307                 if (mSetupUtils.isSetupDone(inputId) || channelCount > 0) {
    308                     if (channelCount == 0) {
    309                         description = getString(R.string.setup_input_no_channels);
    310                     } else {
    311                         description = getResources().getQuantityString(
    312                                 R.plurals.setup_input_channels, channelCount, channelCount);
    313                     }
    314                 } else if (i >= mKnownInputStartIndex) {
    315                     description = getString(R.string.setup_input_setup_now);
    316                 } else {
    317                     description = getString(R.string.setup_input_new);
    318                 }
    319                 ++position;
    320                 if (input.getId().equals(mNewlyAddedInputId)) {
    321                     newPosition = position;
    322                 }
    323                 actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_INPUT_START + i)
    324                         .title(input.loadLabel(getActivity()).toString()).description(description)
    325                         .build());
    326             }
    327             if (Features.ONBOARDING_PLAY_STORE.isEnabled(getActivity())) {
    328                 if (mInputs.size() > 0) {
    329                     // Divider
    330                     ++position;
    331                     actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_DIVIDER)
    332                             .title(null).description(null).focusable(false).build());
    333                 }
    334                 // Play store action
    335                 ++position;
    336                 actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_PLAY_STORE)
    337                         .title(getString(R.string.setup_play_store_action_title))
    338                         .description(getString(R.string.setup_play_store_action_description))
    339                         .icon(R.drawable.ic_playstore).build());
    340             }
    341             if (newPosition != -1) {
    342                 VerticalGridView gridView = getGuidedActionsStylist().getActionsGridView();
    343                 gridView.setSelectedPosition(newPosition);
    344             }
    345         }
    346 
    347         @Override
    348         protected String getActionCategory() {
    349             return ACTION_CATEGORY;
    350         }
    351 
    352         @Override
    353         public void onGuidedActionClicked(GuidedAction action) {
    354             if (action.getId() == ACTION_PLAY_STORE) {
    355                 mParentFragment.onActionClick(ACTION_CATEGORY, (int) action.getId());
    356                 return;
    357             }
    358             TvInputInfo input = mInputs.get((int) action.getId() - ACTION_INPUT_START);
    359             if (mParentFragment.mInputSetupRunnable != null) {
    360                 mParentFragment.mInputSetupRunnable.runInputSetup(input);
    361                 return;
    362             }
    363             Intent intent = TvCommonUtils.createSetupIntent(input);
    364             if (intent == null) {
    365                 Toast.makeText(getActivity(), R.string.msg_no_setup_activity, Toast.LENGTH_SHORT)
    366                         .show();
    367                 return;
    368             }
    369             // Even though other app can handle the intent, the setup launched by Live channels
    370             // should go through Live channels SetupPassthroughActivity.
    371             intent.setComponent(new ComponentName(getActivity(), SetupPassthroughActivity.class));
    372             try {
    373                 // Now we know that the user intends to set up this input. Grant permission for
    374                 // writing EPG data.
    375                 SetupUtils.grantEpgPermission(getActivity(), input.getServiceInfo().packageName);
    376                 startActivityForResult(intent, REQUEST_CODE_START_SETUP_ACTIVITY);
    377             } catch (ActivityNotFoundException e) {
    378                 Toast.makeText(getActivity(), getString(R.string.msg_unable_to_start_setup_activity,
    379                         input.loadLabel(getActivity())), Toast.LENGTH_SHORT).show();
    380             }
    381         }
    382 
    383         @Override
    384         public void onActivityResult(int requestCode, int resultCode, Intent data) {
    385             updateActions();
    386         }
    387 
    388         void executePendingAction() {
    389             switch (mPendingAction) {
    390                 case PENDING_ACTION_INPUT_CHANGED:
    391                     buildInputs();
    392                     // Fall through
    393                 case PENDING_ACTION_CHANNEL_CHANGED:
    394                     updateActions();
    395                     break;
    396             }
    397             mPendingAction = PENDING_ACTION_NONE;
    398         }
    399 
    400         private class SetupSourceGuidedActionsStylist extends GuidedActionsStylist {
    401             private static final int VIEW_TYPE_DIVIDER = 1;
    402 
    403             private static final float ALPHA_CATEGORY = 1.0f;
    404             private static final float ALPHA_INPUT_DESCRIPTION = 0.5f;
    405 
    406             @Override
    407             public int getItemViewType(GuidedAction action) {
    408                 if (action.getId() == ACTION_DIVIDER) {
    409                     return VIEW_TYPE_DIVIDER;
    410                 }
    411                 return super.getItemViewType(action);
    412             }
    413 
    414             @Override
    415             public int onProvideItemLayoutId(int viewType) {
    416                 if (viewType == VIEW_TYPE_DIVIDER) {
    417                     return R.layout.onboarding_item_divider;
    418                 }
    419                 return super.onProvideItemLayoutId(viewType);
    420             }
    421 
    422             @Override
    423             public void onBindViewHolder(ViewHolder vh, GuidedAction action) {
    424                 super.onBindViewHolder(vh, action);
    425                 TextView descriptionView = vh.getDescriptionView();
    426                 if (descriptionView != null) {
    427                     if (action.getId() == ACTION_HEADER) {
    428                         descriptionView.setAlpha(ALPHA_CATEGORY);
    429                         descriptionView.setTextColor(Utils.getColor(getResources(),
    430                                 R.color.setup_category));
    431                         descriptionView.setTypeface(Typeface.create(
    432                                 getString(R.string.condensed_font), 0));
    433                     } else {
    434                         descriptionView.setAlpha(ALPHA_INPUT_DESCRIPTION);
    435                         descriptionView.setTextColor(Utils.getColor(getResources(),
    436                                 R.color.common_setup_input_description));
    437                         descriptionView.setTypeface(Typeface.create(getString(R.string.font), 0));
    438                     }
    439                 }
    440                 // Workaround for b/26473407.
    441                 ImageView iconView = vh.getIconView();
    442                 if (iconView != null) {
    443                     Drawable icon = action.getIcon();
    444                     if (icon != null) {
    445                         // setImageDrawable resets the drawable's level unless we set the view level
    446                         // first.
    447                         iconView.setImageLevel(icon.getLevel());
    448                         iconView.setImageDrawable(icon);
    449                         iconView.setVisibility(View.VISIBLE);
    450                     } else {
    451                         iconView.setVisibility(View.GONE);
    452                     }
    453                 }
    454             }
    455         }
    456     }
    457 }
    458