/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tv.onboarding; import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager.TvInputCallback; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; import android.support.v17.leanback.widget.GuidedActionsStylist; import android.support.v17.leanback.widget.VerticalGridView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import com.android.tv.ApplicationSingletons; import com.android.tv.Features; import com.android.tv.R; import com.android.tv.SetupPassthroughActivity; import com.android.tv.TvApplication; import com.android.tv.common.TvCommonUtils; import com.android.tv.common.ui.setup.SetupGuidedStepFragment; import com.android.tv.common.ui.setup.SetupMultiPaneFragment; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.TvInputNewComparator; import com.android.tv.util.SetupUtils; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * A fragment for channel source info/setup. */ public class SetupSourcesFragment extends SetupMultiPaneFragment { private static final String TAG = "SetupSourcesFragment"; public static final String ACTION_CATEGORY = "com.android.tv.onboarding.SetupSourcesFragment"; public static final int ACTION_PLAY_STORE = 1; private static final String SETUP_TRACKER_LABEL = "Setup fragment"; private InputSetupRunnable mInputSetupRunnable; private ContentFragment mContentFragment; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = super.onCreateView(inflater, container, savedInstanceState); TvApplication.getSingletons(getActivity()).getTracker().sendScreenView(SETUP_TRACKER_LABEL); return view; } @Override protected void onEnterTransitionEnd() { if (mContentFragment != null) { mContentFragment.executePendingAction(); } } @Override protected SetupGuidedStepFragment onCreateContentFragment() { mContentFragment = new ContentFragment(); mContentFragment.setParentFragment(this); Bundle arguments = new Bundle(); arguments.putBoolean(SetupGuidedStepFragment.KEY_THREE_PANE, true); mContentFragment.setArguments(arguments); return mContentFragment; } @Override protected String getActionCategory() { return ACTION_CATEGORY; } /** * Call this method to run customized input setup. * * @param runnable runnable to be called when the input setup is necessary. */ public void setInputSetupRunnable(InputSetupRunnable runnable) { mInputSetupRunnable = runnable; } /** * Interface for the customized input setup. */ public interface InputSetupRunnable { /** * Called for the input setup. * * @param input TV input for setup. */ void runInputSetup(TvInputInfo input); } public static class ContentFragment extends SetupGuidedStepFragment { private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1; // ACTION_PLAY_STORE is defined in the outer class. private static final int ACTION_DIVIDER = 2; private static final int ACTION_HEADER = 3; private static final int ACTION_INPUT_START = 4; private static final int PENDING_ACTION_NONE = 0; private static final int PENDING_ACTION_INPUT_CHANGED = 1; private static final int PENDING_ACTION_CHANNEL_CHANGED = 2; private TvInputManagerHelper mInputManager; private ChannelDataManager mChannelDataManager; private SetupUtils mSetupUtils; private List mInputs; private int mKnownInputStartIndex; private int mDoneInputStartIndex; private SetupSourcesFragment mParentFragment; private String mNewlyAddedInputId; private int mPendingAction = PENDING_ACTION_NONE; private final TvInputCallback mInputCallback = new TvInputCallback() { @Override public void onInputAdded(String inputId) { handleInputChanged(); } @Override public void onInputRemoved(String inputId) { handleInputChanged(); } @Override public void onInputUpdated(String inputId) { handleInputChanged(); } private void handleInputChanged() { // The actions created while enter transition is running will not be included in the // fragment transition. if (mParentFragment.isEnterTransitionRunning()) { mPendingAction = PENDING_ACTION_INPUT_CHANGED; return; } buildInputs(); updateActions(); } }; void setParentFragment(SetupSourcesFragment parentFragment) { mParentFragment = parentFragment; } private final ChannelDataManager.Listener mChannelDataManagerListener = new ChannelDataManager.Listener() { @Override public void onLoadFinished() { handleChannelChanged(); } @Override public void onChannelListUpdated() { handleChannelChanged(); } @Override public void onChannelBrowsableChanged() { handleChannelChanged(); } private void handleChannelChanged() { // The actions created while enter transition is running will not be included in the // fragment transition. if (mParentFragment.isEnterTransitionRunning()) { if (mPendingAction != PENDING_ACTION_INPUT_CHANGED) { mPendingAction = PENDING_ACTION_CHANNEL_CHANGED; } return; } updateActions(); } }; @Override public void onCreate(Bundle savedInstanceState) { // TODO: Handle USB TV tuner differently. Context context = getActivity(); ApplicationSingletons app = TvApplication.getSingletons(context); mInputManager = app.getTvInputManagerHelper(); mChannelDataManager = app.getChannelDataManager(); mSetupUtils = SetupUtils.getInstance(context); buildInputs(); mInputManager.addCallback(mInputCallback); mChannelDataManager.addListener(mChannelDataManagerListener); super.onCreate(savedInstanceState); } @Override public void onDestroy() { super.onDestroy(); mChannelDataManager.removeListener(mChannelDataManagerListener); mInputManager.removeCallback(mInputCallback); } @NonNull @Override public Guidance onCreateGuidance(Bundle savedInstanceState) { String title = getString(R.string.setup_sources_text); String description = getString(R.string.setup_sources_description); return new Guidance(title, description, null, null); } @Override public GuidedActionsStylist onCreateActionsStylist() { return new SetupSourceGuidedActionsStylist(); } @Override public void onCreateActions(@NonNull List actions, Bundle savedInstanceState) { createActionsInternal(actions); } private void buildInputs() { List oldInputs = mInputs; mInputs = mInputManager.getTvInputInfos(true, true); // Get newly installed input ID. if (oldInputs != null) { List newList = new ArrayList<>(mInputs); for (TvInputInfo input : oldInputs) { newList.remove(input); } if (newList.size() > 0 && mSetupUtils.isNewInput(newList.get(0).getId())) { mNewlyAddedInputId = newList.get(0).getId(); } else { mNewlyAddedInputId = null; } } Collections.sort(mInputs, new TvInputNewComparator(mSetupUtils, mInputManager)); mKnownInputStartIndex = 0; mDoneInputStartIndex = 0; for (TvInputInfo input : mInputs) { if (mSetupUtils.isNewInput(input.getId())) { mSetupUtils.markAsKnownInput(input.getId()); ++mKnownInputStartIndex; } if (!mSetupUtils.isSetupDone(input.getId())) { ++mDoneInputStartIndex; } } } private void updateActions() { List actions = new ArrayList<>(); createActionsInternal(actions); setActions(actions); } private void createActionsInternal(List actions) { int newPosition = -1; int position = 0; if (mDoneInputStartIndex > 0) { // Need a "New" category actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_HEADER) .title(null).description(getString(R.string.setup_category_new)) .focusable(false).build()); } for (int i = 0; i < mInputs.size(); ++i) { if (i == mDoneInputStartIndex) { ++position; actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_HEADER) .title(null).description(getString(R.string.setup_category_done)) .focusable(false).build()); } TvInputInfo input = mInputs.get(i); String inputId = input.getId(); String description; int channelCount = mChannelDataManager.getChannelCountForInput(inputId); if (mSetupUtils.isSetupDone(inputId) || channelCount > 0) { if (channelCount == 0) { description = getString(R.string.setup_input_no_channels); } else { description = getResources().getQuantityString( R.plurals.setup_input_channels, channelCount, channelCount); } } else if (i >= mKnownInputStartIndex) { description = getString(R.string.setup_input_setup_now); } else { description = getString(R.string.setup_input_new); } ++position; if (input.getId().equals(mNewlyAddedInputId)) { newPosition = position; } actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_INPUT_START + i) .title(input.loadLabel(getActivity()).toString()).description(description) .build()); } if (Features.ONBOARDING_PLAY_STORE.isEnabled(getActivity())) { if (mInputs.size() > 0) { // Divider ++position; actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_DIVIDER) .title(null).description(null).focusable(false).build()); } // Play store action ++position; actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_PLAY_STORE) .title(getString(R.string.setup_play_store_action_title)) .description(getString(R.string.setup_play_store_action_description)) .icon(R.drawable.ic_playstore).build()); } if (newPosition != -1) { VerticalGridView gridView = getGuidedActionsStylist().getActionsGridView(); gridView.setSelectedPosition(newPosition); } } @Override protected String getActionCategory() { return ACTION_CATEGORY; } @Override public void onGuidedActionClicked(GuidedAction action) { if (action.getId() == ACTION_PLAY_STORE) { mParentFragment.onActionClick(ACTION_CATEGORY, (int) action.getId()); return; } TvInputInfo input = mInputs.get((int) action.getId() - ACTION_INPUT_START); if (mParentFragment.mInputSetupRunnable != null) { mParentFragment.mInputSetupRunnable.runInputSetup(input); return; } Intent intent = TvCommonUtils.createSetupIntent(input); if (intent == null) { Toast.makeText(getActivity(), R.string.msg_no_setup_activity, Toast.LENGTH_SHORT) .show(); return; } // Even though other app can handle the intent, the setup launched by Live channels // should go through Live channels SetupPassthroughActivity. intent.setComponent(new ComponentName(getActivity(), SetupPassthroughActivity.class)); try { // Now we know that the user intends to set up this input. Grant permission for // writing EPG data. SetupUtils.grantEpgPermission(getActivity(), input.getServiceInfo().packageName); startActivityForResult(intent, REQUEST_CODE_START_SETUP_ACTIVITY); } catch (ActivityNotFoundException e) { Toast.makeText(getActivity(), getString(R.string.msg_unable_to_start_setup_activity, input.loadLabel(getActivity())), Toast.LENGTH_SHORT).show(); } } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { updateActions(); } void executePendingAction() { switch (mPendingAction) { case PENDING_ACTION_INPUT_CHANGED: buildInputs(); // Fall through case PENDING_ACTION_CHANNEL_CHANGED: updateActions(); break; } mPendingAction = PENDING_ACTION_NONE; } private class SetupSourceGuidedActionsStylist extends GuidedActionsStylist { private static final int VIEW_TYPE_DIVIDER = 1; private static final float ALPHA_CATEGORY = 1.0f; private static final float ALPHA_INPUT_DESCRIPTION = 0.5f; @Override public int getItemViewType(GuidedAction action) { if (action.getId() == ACTION_DIVIDER) { return VIEW_TYPE_DIVIDER; } return super.getItemViewType(action); } @Override public int onProvideItemLayoutId(int viewType) { if (viewType == VIEW_TYPE_DIVIDER) { return R.layout.onboarding_item_divider; } return super.onProvideItemLayoutId(viewType); } @Override public void onBindViewHolder(ViewHolder vh, GuidedAction action) { super.onBindViewHolder(vh, action); TextView descriptionView = vh.getDescriptionView(); if (descriptionView != null) { if (action.getId() == ACTION_HEADER) { descriptionView.setAlpha(ALPHA_CATEGORY); descriptionView.setTextColor(Utils.getColor(getResources(), R.color.setup_category)); descriptionView.setTypeface(Typeface.create( getString(R.string.condensed_font), 0)); } else { descriptionView.setAlpha(ALPHA_INPUT_DESCRIPTION); descriptionView.setTextColor(Utils.getColor(getResources(), R.color.common_setup_input_description)); descriptionView.setTypeface(Typeface.create(getString(R.string.font), 0)); } } // Workaround for b/26473407. ImageView iconView = vh.getIconView(); if (iconView != null) { Drawable icon = action.getIcon(); if (icon != null) { // setImageDrawable resets the drawable's level unless we set the view level // first. iconView.setImageLevel(icon.getLevel()); iconView.setImageDrawable(icon); iconView.setVisibility(View.VISIBLE); } else { iconView.setVisibility(View.GONE); } } } } } }